簡體   English   中英

在現代x86上有哪些方法可以有效地擴展指令長度?

[英]What methods can be used to efficiently extend instruction length on modern x86?

想象一下,您希望將一系列x86匯編指令與某些邊界對齊。 例如,您可能希望將循環對齊到16或32字節的邊界,或者將指令打包以使它們有效地放置在uop緩存中或其他任何位置。

實現這一目標的最簡單方法是單字節NOP指令,緊接着是多字節NOP 雖然后者通常效率更高,但這兩種方法都不是免費的:NOP使用前端執行資源,並且還計入現代x86上的4寬1重命名限制。

另一個選擇是以某種方式延長一些指令以獲得所需的對齊。 如果這樣做沒有引入新的停頓,它似乎比NOP方法更好。 如何在最近的x86 CPU上有效地延長指令?

在理想的世界中,延長技術同時是:

  • 適用於大多數說明
  • 能夠通過可變數量延長指令
  • 不會停止或以其他方式減慢解碼器的速度
  • 在uop緩存中有效表示

有一種方法不可能同時滿足所有上述要點,因此很好的答案可能會解決各種權衡問題。


1 AMD Ryzen的限制為5或6。

考慮使用溫和的代碼 - 高爾夫來縮小代碼而不是擴展代碼 ,特別是在循環之前。 例如xor eax,eax / cdq如果需要兩個歸零寄存器,或者移動mov eax, 1 / lea ecx, [rax+1]將寄存器設置為1和2,總共只有8個字節而不是10個。請參見設置CPU中的所有位有效注冊1以獲得更多相關信息,以及x86 / x64機器代碼中的高爾夫球技巧提示以獲得更多一般性創意。 不過,可能你仍然希望避免錯誤的依賴。

或者通過動態創建向量常量來填充額外的空間,而不是從內存中加載它。 (但是,對於包含setup +內部循環的較大循環,添加更多uop-cache壓力可能會更糟。但它避免了常量的d-cache錯誤,因此它有一個上升空間來補償運行更多的uops。)

如果您尚未使用它們來加載“壓縮”常量,則pmovsxbdmovddupvpbroadcastdmovaps更長。 dword / qword廣播加載是免費的(沒有ALU uop,只是一個加載)。

如果你擔心代碼對齊,你可能會擔心它在L1I緩存中的位置或者uop-cache邊界的位置,所以只計算總uops不再足夠,並且還有一些額外的uops。 你關心的人之前阻止可能根本不是問題。

但在某些情況下,您可能真的希望在要對齊的塊之前優化指令的解碼吞吐量/ uop-cache使用/總uop。


填充說明,如要求的問題:

Agner Fog在他的“匯編語言中的優化子程序”指南中 有一個完整的部分:“10.6為了對齊而制作更長的指令 leapush r/m64和SIB的想法來自那里,我復制了一個或兩個短語,否則這個答案是我自己的作品,要么是不同的想法,要么是在檢查Agner指南之前寫的。)

雖然它還沒有為當前的CPU更新,但是: lea eax, [rbx + dword 0]比使用mov eax, ebx有更多的缺點,因為你錯過了零延遲/沒有執行單元mov 如果它不在關鍵路徑上,那就去吧。 簡單的lea具有相當好的吞吐量,並且具有大尋址模式(甚至可能甚至一些段前綴)的LEA可以比mov + nop更好地用於解碼/執行吞吐量。

使用一般形式而不是簡短形式(無ModR / M)的指令,如push regmov reg,imm 例如,對於push rbx使用2字節push r/m64 或者使用更長的等效指令,例如add dst, 1而不是inc dst如果沒有inc缺點,那么你已經在使用inc

使用SIB字節 您可以通過使用單個寄存器作為索引來讓NASM這樣做,例如mov eax, [nosplit rbx*1]另請參閱 ),但這會影響負載使用延遲,而不僅僅是編碼mov eax, [rbx]一個SIB字節。 索引尋址模式在SnB系列上有其他缺點, 例如取消層壓而不使用port7用於存儲

因此,最好只使用沒有索引reg的ModR / M + SIB對base=rbx + disp0/8/32=0進行編碼 (“無索引”的SIB編碼是否則意味着idx = RSP的編碼)。 [rsp + x]尋址模式已經需要一個SIB(base = RSP是轉義代碼,意味着有一個SIB),並且它始終出現在編譯器生成的代碼中。 因此,現在和未來都有充分的理由期望它能夠完全有效地解碼和執行(即使對於RSP以外的基址寄存器)。 NASM語法無法表達這一點,因此您必須手動編碼。 來自objdump -d GNU gas Intel語法為Agner Fog的例子10.20表示8b 04 23 mov eax,DWORD PTR [rbx+riz*1] riz是一個虛構的索引 - 零表示法,意味着有一個沒有索引的SIB)。 我沒有測試過GAS是否接受它作為輸入。

使用只需要imm8disp0/disp32的指令的imm32和/或disp32形式。 Agner Fog對Sandybridge的uop緩存( 微指南指南表9.1 )的測試表明,立即/位移的實際值是重要的,而不是指令編碼中使用的字節數。 我沒有關於Ryzen的uop緩存的任何信息。

所以NASM imul eax, [dword 4 + rdi], strict dword 13 (10字節:操作碼+ modrm + disp32 + imm32)將使用32small,32small類別並在uop緩存中取1個條目,這與immediate或disp32不同實際上有超過16個有效位。 (然后它將需要2個條目,並從uop緩存加載它將需要一個額外的周期。)

根據Agner的表,8/16 / 32small總是相當於SnB。 並且具有寄存器的尋址模式是相同的,無論是否完全沒有位移,或者它是否為32small,因此mov dword [dword 0 + rdi], 123456需要2個條目,就像mov dword [rdi], 123456789 我沒有意識到[rdi] +完整的imm32占用了2個條目,但顯然在SnB上就是這種情況。

使用jmp / jcc rel32而不是rel8 理想情況下,嘗試在不需要在您正在擴展的區域之外進行更長跳轉編碼的位置擴展指令。 在跳躍目標之后填充較早的前向跳躍,在跳躍目標之前填充以便稍后向后跳躍,如果它們接近需要在其他地方使用rel32。 即嘗試避免分支與其目標之間的填充,除非您希望該分支仍然使用rel32。


你可能想要將mov eax, [symbol]編碼為6字節a32 mov eax, [abs symbol] 64位代碼中的a32 mov eax, [abs symbol] ,使用地址大小前綴來使用32位絕對地址。 但是,當它在Intel CPU上解碼時, 這確實會導致長度變化前綴失速 幸運的是,如果沒有明確指定32位地址大小,而不是使用7字節mov r32, r/m32和ModR / M,則默認情況下NASM / YASM / gas / clang都不執行此代碼大小優化mov eax, [abs symbol] + SIB + disp32絕對尋址模式mov eax, [abs symbol]

在64位位置相關代碼中,絕對尋址是使用1個額外字節而不是RIP相對的廉價方式 但請注意,32位絕對+立即需要2個周期才能從uop緩存中獲取,這與RIP相對+ imm8 / 16/32不同,后者只需1個周期,即使它仍然使用2個條目作為指令。 (例如,對於mov -store或cmp )。 所以cmp [abs symbol], 123從uop緩存中獲取的速度比cmp [rel symbol], 123慢,即使每個都需要2個條目。 沒有立即,沒有額外的成本

請注意,PIE可執行文件甚至允許ASLR用於可執行文件, 並且是許多Linux發行版中的默認設置 ,因此如果您可以保持代碼PIC沒有任何缺陷,那么這是更可取的。


當您不需要時,請使用REX前綴,例如db 0x40 / add eax, ecx

添加當前CPU忽略的rep這樣的前綴通常不安全,因為它們可能在未來的ISA擴展中意味着其他東西。

有時可能會重復相同的前綴(但不能使用REX)。 例如, db 0x66, 0x66 / add ax, bx給出指令3操作數大小的前綴,我認為它總是嚴格等同於前綴的一個副本。 最多3個前綴是某些CPU上有效解碼的限制。 但這只適用於你有一個前綴,你可以在第一時間使用; 您通常不使用16位操作數大小,並且通常不需要32位地址大小(盡管在位置相關代碼中訪問靜態數據是安全的)。

訪問內存的指令上的dsss前綴是無操作 ,並且可能不會導致任何當前CPU的任何減速。 (@prl在評論中建議這一點)。

實際上, Agner Fog的微指南在例7.1中movq [esi+ecx],mm0上使用了ds前綴 安排IFETCH塊來調整PII / PIII的循環(無循環緩沖區或uop緩存),將其從每個時鍾的3次迭代加速到2。

當指令有超過3個前綴時,某些CPU(如AMD)會慢慢解碼。 在某些CPU上,這包括SSE2中的強制性前綴,尤其是SSSE3 / SSE4.1指令。 在Silvermont中,即使0F轉義字節也很重要。

AVX指令可以使用2或3字節的VEX前綴 某些指令需要3字節的VEX前綴(第二個源是x / ymm8-15,或SSSE3或更高版本的必需前綴)。 但是,可以使用2字節前綴的指令始終可以使用3字節VEX進行編碼。 NASM或GAS {vex3} vxorps xmm0,xmm0 如果AVX512可用,您也可以使用4字節EVEX。


即使你不需要 ,也可以使用64位操作數大小的mov ,例如mov rax, strict dword 1強制在NASM中使用7字節的sign-extended-imm32編碼, 這通常會將其優化為5字節的mov eax, 1

mov    eax, 1                ; 5 bytes to encode (B8 imm32)
mov    rax, strict dword 1   ; 7 bytes: REX mov r/m64, sign-extended-imm32.
mov    rax, strict qword 1   ; 10 bytes to encode (REX B8 imm64).  movabs mnemonic for AT&T.

您甚至可以使用mov reg, 0而不是xor reg,reg

mov r64, imm64在常量實際很小時有效地適合mov r64, imm64緩存(適合32位符號擴展。) 1 uop-cache條目,load-time = 1,與mov r32, imm32 對一個巨大的指令進行解碼意味着在一個16字節的解碼塊中可能沒有空間用於其他3個指令在同一個周期內進行解碼,除非它們都是2字節。 稍微延長多個其他指令可能比使用一個長指令更好。


解碼額外前綴的懲罰:

  • P5:前綴禁止配對,但PMMX上的地址/操作數大小除外。
  • PPro到PIII: 如果一條指令有多個前綴,總會有一個懲罰。 這個懲罰通常是每個額外前綴一個時鍾。 (Agner的微型指南,6.3節末)
  • Silvermont:如果您關心它,它可能是您可以使用哪些前綴的最嚴格限制。 解碼超過3個前綴,計算強制前綴+ 0F轉義字節。 SSSE3和SSE4指令已經有3個前綴,因此即使是REX也會使解碼速度變慢。
  • 某些AMD:可能是3前綴限制, 包括轉義字節,也可能不包括SSE指令的強制性前綴。

... TODO:完成本節。 在此之前,請咨詢Agner Fog的微型指南。


手工編碼之后,總是反匯編你的二進制文件以確保你做對了 令人遺憾的是,NASM和其他裝配工無法更好地支持在指令區域上選擇便宜的填充以達到給定的對齊邊界。


匯編語法

NASM具有一些編碼覆蓋語法{vex3}{evex}前綴, NOSPLITstrict byte / dword ,並強制調用disp8 / disp32內部尋址模式。 請注意,不允許使用[rdi + byte 0] ,必須先使用byte關鍵字。 [byte rdi + 0]是允許的,但我認為這看起來很奇怪。

nasm -l/dev/stdout -felf64 padding.asm

 line  addr    machine-code bytes      source line
 num

 4 00000000 0F57C0                         xorps  xmm0,xmm0    ; SSE1 *ps instructions are 1-byte shorter
 5 00000003 660FEFC0                       pxor   xmm0,xmm0
 6                                  
 7 00000007 C5F058DA                       vaddps xmm3, xmm1,xmm2
 8 0000000B C4E17058DA              {vex3} vaddps xmm3, xmm1,xmm2
 9 00000010 62F1740858DA            {evex} vaddps xmm3, xmm1,xmm2
10                                  
11                                  
12 00000016 FFC0                        inc  eax
13 00000018 83C001                      add  eax, 1
14 0000001B 4883C001                    add  rax, 1
15 0000001F 678D4001                    lea  eax, [eax+1]     ; runs on fewer ports and doesn't set flags
16 00000023 67488D4001                  lea  rax, [eax+1]     ; address-size and REX.W
17 00000028 0501000000                  add  eax, strict dword 1   ; using the EAX-only encoding with no ModR/M 
18 0000002D 81C001000000                db 0x81, 0xC0, 1,0,0,0     ; add    eax,0x1  using the ModR/M imm32 encoding
19 00000033 81C101000000                add  ecx, strict dword 1   ; non-eax must use the ModR/M encoding
20 00000039 4881C101000000              add  rcx, strict qword 1   ; YASM requires strict dword for the immediate, because it's still 32b
21 00000040 67488D8001000000            lea  rax, [dword eax+1]
22                                  
23                                  
24 00000048 8B07                        mov  eax, [rdi]
25 0000004A 8B4700                      mov  eax, [byte 0 + rdi]
26 0000004D 3E8B4700                    mov  eax, [ds: byte 0 + rdi]
26          ******************       warning: ds segment base generated, but will be ignored in 64-bit mode
27 00000051 8B8700000000                mov  eax, [dword 0 + rdi]
28 00000057 8B043D00000000              mov  eax, [NOSPLIT dword 0 + rdi*1]  ; 1c extra latency on SnB-family for non-simple addressing mode

GAS具有編碼覆蓋偽前綴 {vex3}{evex}{disp8}{disp32} 這些替換現在已棄用的.s.d8.d32后綴

GAS沒有覆蓋直接大小,只有位移。

GAS允許您使用ds mov src,dst添加顯式ds前綴

gcc -g -c padding.S && objdump -drwC padding.o -S ,帶有手工編輯:

  # no CPUs have separate ps vs. pd domains, so there's no penalty for mixing ps and pd loads/shuffles
  0:   0f 28 07                movaps (%rdi),%xmm0
  3:   66 0f 28 07             movapd (%rdi),%xmm0

  7:   0f 58 c8                addps  %xmm0,%xmm1        # not equivalent for SSE/AVX transitions, but sometimes safe to mix with AVX-128

  a:   c5 e8 58 d9             vaddps %xmm1,%xmm2, %xmm3  # default {vex2}
  e:   c4 e1 68 58 d9          {vex3} vaddps %xmm1,%xmm2, %xmm3
 13:   62 f1 6c 08 58 d9       {evex} vaddps %xmm1,%xmm2, %xmm3

 19:   ff c0                   inc    %eax
 1b:   83 c0 01                add    $0x1,%eax
 1e:   48 83 c0 01             add    $0x1,%rax
 22:   67 8d 40 01             lea  1(%eax), %eax     # runs on fewer ports and doesn't set flags
 26:   67 48 8d 40 01          lea  1(%eax), %rax     # address-size and REX
         # no equivalent for  add  eax, strict dword 1   # no-ModR/M

         .byte 0x81, 0xC0; .long 1    # add    eax,0x1  using the ModR/M imm32 encoding
 2b:   81 c0 01 00 00 00       add    $0x1,%eax     # manually encoded
 31:   81 c1 d2 04 00 00       add    $0x4d2,%ecx   # large immediate, can't get GAS to encode this way with $1 other than doing it manually

 37:   67 8d 80 01 00 00 00      {disp32} lea  1(%eax), %eax
 3e:   67 48 8d 80 01 00 00 00   {disp32} lea  1(%eax), %rax


        mov  0(%rdi), %eax      # the 0 optimizes away
  46:   8b 07                   mov    (%rdi),%eax
{disp8}  mov  (%rdi), %eax      # adds a disp8 even if you omit the 0
  48:   8b 47 00                mov    0x0(%rdi),%eax
{disp8}  ds mov  (%rdi), %eax   # with a DS prefix
  4b:   3e 8b 47 00             mov    %ds:0x0(%rdi),%eax
{disp32} mov  (%rdi), %eax
  4f:   8b 87 00 00 00 00       mov    0x0(%rdi),%eax
{disp32} mov  0(,%rdi,1), %eax    # 1c extra latency on SnB-family for non-simple addressing mode
  55:   8b 04 3d 00 00 00 00    mov    0x0(,%rdi,1),%eax

GAS嚴格來說不像NASM那樣表達超過需要的編碼。

我可以想到四個方面:

第一:使用備用編碼作為指令(Peter Cordes提到類似的東西)。 例如,有很多方法可以調用ADD操作,其中一些方法占用更多字節:

http://www.felixcloutier.com/x86/ADD.html

通常,匯編程序會嘗試為這種情況選擇“最佳”編碼,無論是針對速度還是長度進行優化,但是您總是可以使用另一個編碼並獲得相同的結果。

第二:使用意思相同且長度不同的其他說明。 我相信你可以想到無數的例子,你可以將一條指令放入代碼中來替換現有的指令並得到相同的結果。 手動優化代碼的人會一直這樣做:

shl 1
add eax, eax
mul 2
etc etc

第三:使用各種NOP來填補額外空間:

nop
and eax, eax
sub eax, 0
etc etc

在理想的世界中,您可能必須使用所有這些技巧來使代碼成為您想要的確切字節長度。

第四:使用上述方法更改算法以獲得更多選項。

最后一點:由於指令的數量和復雜性,顯然針對更現代的處理器將為您提供更好的結果。 訪問MMX,XMM,SSE,SSE2,浮點等指令可以使您的工作更輕松。

我們來看一段特定的代碼:

    cmp ebx,123456
    mov al,0xFF
    je .foo

對於此代碼,沒有任何指令可以替換為其他任何指令,因此唯一的選項是冗余前綴和NOP。

但是,如果更改指令順序怎么辦?

您可以將代碼轉換為:

    mov al,0xFF
    cmp ebx,123456
    je .foo

重新訂購說明后; mov al,0xFF可以用or eax,0x000000FF or ax,0x00FF替換。

對於第一個指令排序,只有一種可能性,對於第二個指令排序,有3種可能性; 因此,在不使用任何冗余前綴或NOP的情況下,總共有4種可能的排列可供選擇。

對於這4種排列中的每一種,您可以添加具有不同冗余前綴量的變體,以及單字節和多字節NOP,以使其以特定對齊結束。 我懶得做數學,所以讓我們假設它可能擴展到100種可能的排列。

如果你給這100個排列中的每一個都得分(基於諸如執行需要多長時間,如果大小或速度很重要的話,如果大小或速度很重要,它與指令對齊的程度等等),該怎么辦? 這可以包括微架構定位(例如,對於某些CPU,原始排列會破壞微操作融合並使代碼變得更糟)。

您可以生成所有可能的排列並給它們一個分數,並選擇具有最佳分數的排列。 請注意,這可能不是具有最佳對齊的排列(如果對齊不如其他因素重要,只會使性能變差)。

當然,您可以將大型程序分成許多由控制流變化分隔的小型線性指令組; 然后對每組小線性指令進行“窮舉搜索最佳得分排列”。

問題是指令順序和指令選擇是相互依賴的。

對於上面的示例,在我們重新排序指令之前,您無法替換mov al,0xFF ; 並且很容易找到在您更換(某些)指令之后無法重新訂購指令的情況。 這使得很難對“最佳”的任何定義進行詳盡的搜索,即使您只關心對齊而根本不關心性能。

取決於代碼的性質。

浮點重碼

AVX前綴

對於大多數SSE指令,可以使用更長的AVX前綴。 請注意,在intel CPU上切換SSE和AVX時存在固定的損失[1] [2] 這需要vzeroupper,它可以被解釋為SSE代碼的另一個NOP或不需要更高128位的AVX代碼。

SSE / AVX NOPS

我能想到的典型NOP是:

  • 對相同的寄存器進行XORPS,對這些的整數使用SSE / AVX變體
  • ANDPS使用相同的寄存器,對這些的整數使用SSE / AVX變體

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM