代码之家  ›  专栏  ›  技术社区  ›  BeeOnRope

在现代x86上,可以使用哪些方法有效地扩展指令长度?

  •  23
  • BeeOnRope  · 技术社区  · 7 年前

    假设您希望将一系列x86汇编指令与特定边界对齐。例如,您可能希望将循环与16或32字节的边界对齐,或者打包指令,以便有效地将它们放置在uop缓存或其他任何地方。

    实现这一点的最简单方法是单字节NOP指令,后面紧跟着 multi-byte NOPs . 尽管后者通常效率更高,但这两种方法都不是免费的:NOP使用前端执行资源,并且还可以与您的4-wide 1. 重命名现代x86上的限制。

    另一种选择是以某种方式延长一些指令,以获得所需的对齐方式。如果在不引入新摊位的情况下做到这一点,似乎比NOP方法要好。如何在最新的x86 CPU上有效地延长指令?

    在理想情况下,延长技术应同时为:

    • 适用于大多数说明
    • 能够将指令延长可变长度
    • 不要暂停或以其他方式减慢解码器的速度
    • 在uop缓存中有效表示

    不太可能有一种方法同时满足上述所有点,因此好的答案可能会解决各种权衡问题。


    1. AMD Ryzen的限制为5或6。

    4 回复  |  直到 7 年前
        1
  •  10
  •   Peter Cordes    6 年前

    考虑轻度代码高尔夫 收缩 而不是扩展代码 ,尤其是在循环之前。例如 xor eax,eax / cdq 如果需要两个调零寄存器,或 mov eax, 1 / lea ecx, [rax+1] 将寄存器设置为1和2,总字节数仅为8,而不是10。看见 Set all bits in CPU register to 1 efficiently 了解更多信息 Tips for golfing in x86/x64 machine code 了解更一般的想法。不过,您可能仍然希望避免错误的依赖关系。

    或通过以下方式填充额外空间 creating a vector constant on the fly 而不是从内存中加载。(不过,对于包含设置+内部循环的较大循环,增加更多的uop缓存压力可能会更糟。但它可以避免常量的d-cache未命中,因此可以弥补运行更多uop的不足。)

    如果您还没有使用它们加载“压缩”常量, pmovsxbd , movddup vpbroadcastd 长度超过 movaps . dword/qword广播加载是免费的(没有ALU uop,只有加载)。

    如果您担心代码对齐,那么您可能会担心它在L1I缓存中的位置,或者uop缓存边界的位置,因此仅仅计算总uop已经不够了,块中还有一些额外的uop 之前 你关心的人可能根本不是问题。

    但在某些情况下,您可能真的希望在对齐块之前优化指令的解码吞吐量/uop缓存使用率/总uop。


    填充说明,如要求的问题:

    Agner Fog对此有一整节:“10.6为了对齐而延长说明” 在his中 "Optimizing subroutines in assembly language" guide . (The lea , push r/m64 ,SIB的想法来自那里,我抄了一两句话/短语,否则这个答案就是我自己的作品,要么是不同的想法,要么是在查看Agner指南之前写的。)

    当前CPU尚未更新,但: lea eax, [rbx + dword 0] 比过去有更多的缺点 mov eax, ebx ,因为你错过了 zero-latency / no execution unit mov . 如果它不在关键路径上,尽管去做。易于理解的 lea公司 具有相当好的吞吐量,并且具有较大寻址模式(甚至可能有一些段前缀)的LEA在解码/执行吞吐量方面可能比 压敏电阻 + nop .

    使用一般形式,而不是简短形式(无ModR/M),如 push reg mov reg,imm . e、 g.使用2字节 推动r/m64 对于 push rbx . 或者使用更长的等效指令,如 add dst, 1 而不是 inc dst , in cases where there are no perf downsides to inc 所以你已经在使用 股份有限公司 .

    使用SIB字节 . 您可以让NASM通过使用单个寄存器作为索引来实现这一点,如 mov eax, [nosplit rbx*1] ( see also ),但与简单编码相比,这会影响负载使用延迟 mov eax, [rbx] 具有SIB字节。索引寻址模式对SnB系列还有其他缺点, like un-lamination and not using port7 for stores .

    所以 最好只是编码 base=rbx + disp0/8/32=0 使用ModR/M+SIB,无索引reg . (表示“无索引”的SIB编码是表示idx=RSP的编码)。 [rsp + x] 寻址模式已经需要SIB(base=RSP是转义代码,意味着有SIB),并且始终出现在编译器生成的代码中。因此,现在和将来都有充分的理由期望它能够完全高效地解码和执行(即使对于RSP以外的基址寄存器也是如此)。NASM语法无法表达这一点,因此必须手动编码。GNU gas Intel语法来自 objdump -d 8b 04 23 mov eax,DWORD PTR [rbx+riz*1] 对于Agner Fog的示例10.20。( riz 是一个虚构的索引零表示法,意味着有一个没有索引的SIB)。我还没有测试GAS是否接受它作为输入。

    使用 imm32 和/或 disp32 只需要 imm8 disp0/disp32 . Agner Fog对Sandybridge uop缓存的测试( microarch guide table 9.1 )指示重要的是立即数/位移的实际值,而不是指令编码中使用的字节数。我没有关于Ryzen uop缓存的任何信息。

    So NASM imul eax, [dword 4 + rdi], strict dword 13 (10字节:操作码+modrm+disp32+imm32)将使用32small、32small类别,并在uop缓存中获取1个条目,这与立即数或disp32实际具有超过16个有效位的情况不同。(然后需要2个条目,从uop缓存加载它需要额外的周期。)

    根据阿格纳的表格,8/16/32小码总是等同于SnB。寄存器的寻址模式是相同的,无论是完全没有位移,还是很小,所以 mov dword [dword 0 + rdi], 123456 需要2个条目,就像 mov dword [rdi], 123456789 . 我没有意识到 [rdi] +完整的imm32有2个条目,但显然SnB就是这样。

    使用 jmp / jcc rel32 而不是 rel8 . 理想情况下,尝试在您要扩展的区域之外不需要更长跳转编码的地方扩展指令。 先跳后跳,先跳后跳,后跳后跳, 如果他们在其他地方接近需要rel32。i、 e.尽量避免在分支与其目标之间填充,除非您希望该分支使用rel32。


    你可能会想编码 mov eax, [symbol] 作为6字节 a32 mov eax, [abs symbol] 在64位代码中,使用地址大小前缀来使用32位绝对地址。但是 this does cause a Length-Changing-Prefix stall 当它在Intel CPU上解码时。幸运的是,如果不显式指定32位地址大小,而使用7字节,那么默认情况下NASM/YASM/gas/clang都不会进行这种代码大小优化 mov r32, r/m32 使用ModR/M+SIB+disp32绝对寻址模式 mov eax, [abs symbol] .

    在64位位置相关代码中,绝对寻址是使用1个额外字节而不是RIP相对字节的廉价方法 . 但请注意,32位绝对+立即从uop缓存中提取需要2个周期,这与RIP relative+imm8/16/32只需要1个周期不同,尽管它仍然为指令使用2个条目。(例如 压敏电阻 -存储或a cmp ). 所以 cmp [abs symbol], 123 从uop缓存中提取的速度比 cmp [rel symbol], 123 ,即使两者各有2个条目。如果没有即时付款,则无需支付额外费用

    请注意,即使对于可执行文件,PIE可执行文件也允许ASLR, and are the default in many Linux distro ,因此,如果您可以保持代码PIC,而不存在任何性能方面的缺点,那么这是最好的。


    当您不需要REX前缀时,请使用REX前缀,例如。 db 0x40 / add eax, ecx .

    一般来说,添加当前CPU忽略的rep之类的前缀是不安全的,因为它们在未来的ISA扩展中可能意味着其他东西。

    重复相同的前缀有时是可能的(但REX不可能)。例如 db 0x66, 0x66 / add ax, bx 给出指令3操作数大小前缀,我认为它总是严格等价于前缀的一个副本。在某些CPU上,最多3个前缀是有效解码的限制。但这只有在你有一个前缀可以首先使用的情况下才有效;通常不使用16位操作数大小,通常也不需要32位地址大小(尽管在位置相关代码中访问静态数据是安全的)。

    A. ds ss 访问内存的指令的前缀是no op ,并且可能不会导致任何当前CPU上的速度减慢。(@prl在评论中建议)。

    事实上 Agner Fog的微通道指南使用 ds公司 a上的前缀 movq [esi+ecx],mm0 在里面 示例7.1。排列IFETCH块 为PII/PIII(无循环缓冲区或uop缓存)优化循环,将其从每个时钟3次迭代加速到2次。

    有些CPU(如AMD)在指令前缀超过3个时解码速度较慢。在某些CPU上,这包括SSE2中的强制前缀,尤其是SSSE3/SSE4.1指令中的前缀。在Silvermont中,即使是0F转义字节也会计数。

    AVX指令可以使用2或3字节的VEX前缀 . 一些指令需要3字节的VEX前缀(第二个源是x/ymm8-15,或SSSE3或更高版本的强制前缀)。但是,可以使用2字节前缀的指令始终可以使用3字节VEX进行编码。NASM或气体 {vex3} vxorps xmm0,xmm0 . 如果AVX512可用,也可以使用4字节EVEX。


    使用64位操作数大小 压敏电阻 即使你不需要它 例如 mov rax, strict dword 1 强制NASM中的7字节符号扩展imm32编码, which would normally optimize it to 5-byte 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 当常量实际上很小时,可以有效地适应uop缓存(适合32位符号扩展) 1个uop缓存条目,加载时间=1,与 mov r32, imm32 . 对一条巨型指令进行解码意味着,在一个16字节的解码块中,可能没有空间在同一周期内对其他3条指令进行解码,除非它们都是2字节的。可能稍微延长多条其他指令比使用一条长指令要好。


    解码额外前缀的惩罚:

    • P5:前缀阻止配对,只有PMMX上的地址/操作数大小除外。
    • PPro至PIII: 如果一条指令有多个前缀,则总会有一个惩罚。这种惩罚通常是每个额外前缀一个时钟。 (Agner微阵列指南,第6.3节末尾)
    • Silvermont:如果你在乎的话,这可能是对前缀使用的最严格限制。解码暂停超过3个前缀,强制前缀+0个转义字节计数。SSSE3和SSE4指令已经有3个前缀,因此即使是REX也会使其解码速度变慢。
    • 一些AMD:可能有3个前缀的限制, 包括转义字节,可能不包括SSE指令的强制前缀。

    ... 待办事项:完成此部分。在此之前,请参考Agner Fog的Microach指南。


    在手工编码之后,一定要分解二进制文件以确保正确 . 不幸的是,NASM和其他汇编程序无法更好地支持在指令区域上选择廉价的填充来达到给定的对齐边界。


    汇编程序语法

    NASM有一些编码覆盖语法 : {vex3} {evex} 前缀, NOSPLIT strict 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
    

    气体有 encoding-override pseudo-prefixes {vex3} , {evex} , {disp8} {disp32} These replace the now-deprecated .s , .d8 and .d32 suffixes .

    天然气没有对即时大小的覆盖,只有位移。

    GAS确实允许您添加显式 ds公司 前缀,带 ds mov src,dst

    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强大。

        2
  •  1
  •   Brendan    6 年前

    让我们看一段特定的代码:

        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,原始排列可能会破坏微操作融合,使代码变得更糟)。

    您可以生成所有可能的排列并给它们打分,然后选择得分最高的排列。请注意,这可能不是最佳对齐的排列(如果对齐不如其他因素重要,只会使性能更差)。

    当然,您可以将大型程序分解为许多由控制流更改分隔的小型线性指令组;然后对每一小组线性指令进行“穷举搜索具有最佳分数的排列”。

    问题是指令顺序和指令选择是相互依赖的。

    对于上面的示例,您无法替换 移动al,0xFF 直到我们重新订购指示之后;而且很容易找到在更换(一些)说明书之前无法重新订购说明书的情况。这使得很难彻底搜索最佳解决方案,也很难找到“最佳”的任何定义,即使您只关心对齐,而根本不关心性能。

        3
  •  0
  •   Sparafusile    7 年前

    我可以想出四种方法:

    第一: 使用替代编码进行说明(PeterCordes提到了类似的内容)。例如,有很多方法可以调用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、浮点等指令可以使您的工作更轻松。

        4
  •  0
  •   Quonux    6 年前

    取决于代码的性质。

    浮点重码

    AVX前缀

    对于大多数SSE指令,可以使用更长的AVX前缀。 请注意,在intel CPU上在SSE和AVX之间切换时会有一个固定的惩罚 [1] [2] . 这需要vzeroupper,它可以解释为SSE代码或AVX代码的另一个NOP,不需要更高的128位。

    SSE/AVX NOPS

    我能想到的典型NOP有:

    • XORPS相同寄存器,对这些寄存器的整数使用SSE/AVX变体
    • ANDPS同一寄存器,对这些寄存器的整数使用SSE/AVX变量