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

Bee*_*ope 20 optimization performance x86 assembly micro-optimization

想象一下,您希望将一系列x86汇编指令与某些边界对齐.例如,您可能希望将循环对齐到16或32字节的边界,或者将指令打包以使它们有效地放置在uop缓存中或其他任何位置.

实现这一目标的最简单方法是单字节NOP指令,紧接着是多字节NOP.虽然后者通常效率更高,但这两种方法都不是免费的:NOP使用前端执行资源,并且还计入现代x86上的4宽1重命名限制.

另一个选择是以某种方式延长一些指令以获得所需的对齐.如果这样做没有引入新的停顿,它似乎比NOP方法更好.如何在最近的x86 CPU上有效地延长指令?

在理想的世界中,延长技术同时是:

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

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


1 AMD Ryzen的限制为5或6.

Pet*_*des 8

考虑使用温和的代码高尔夫来缩小代码而不是扩展它,尤其是在循环之前。例如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 的好处。)

如果您还没有使用它们来加载“压缩”常量pmovsxbdmovddup、 或vpbroadcastd长于movaps. dword / qword 广播负载是免费的(没有 ALU uop,只是一个负载)。

如果您完全担心代码对齐,您可能会担心它在 L1I 缓存中的位置或 uop 缓存边界在哪里,因此仅计算总 uops 不再足够,并且在你关心的人之前阻止可能根本不是问题。

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


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

Agner Fog在他的“Optimizing subroutines in assembly language”指南中有一整节:“10.6 使指令更长以便对齐”。(本leapush r/m64和SIB的想法是从那里,我复制一个句子/短语或两个,否则,这个答案是我自己的工作,无论是不同的想法或检查昂纳指南之前写的。)

但是,它尚未针对当前 CPU 进行更新: lea eax, [rbx + dword 0]比以前的 vs 有更多的缺点mov eax, ebx,因为您错过了零延迟/无执行单元mov。如果它不在关键路径上,那就去吧。Simplelea具有相当不错的吞吐量,并且具有大寻址模式(甚至可能是某些段前缀)的 LEA 在解码/执行吞吐量方面比mov+更好nop

使用一般形式而不是像push reg或之类的指令的简短形式(无 ModR/M)mov reg,imm。例如使用 2 字节push r/m64push rbx. 或者使用更长的等效指令,例如add dst, 1代替inc dst在没有性能缺点的情况下,inc您已经在使用inc.

使用 SIB 字节。您可以通过使用单个寄存器作为索引来让 NASM 做到这一点,例如mov eax, [nosplit rbx*1]另见),但这会损害负载使用延迟,而不是简单地mov eax, [rbx]使用 SIB 字节进行编码。索引寻址模式在 SnB 系列上还有其他缺点,例如未分层和不使用 port7 进行存储

所以最好只base=rbx + disp0/8/32=0使用没有索引 reg 的 ModR/M + SIB进行编码。(“无索引”的 SIB 编码是否则意味着 idx=RSP 的编码)。 [rsp + x]寻址模式已经需要一个 SIB(base=RSP 是转义码,这意味着有一个 SIB),并且它一直出现在编译器生成的代码中。因此,我们有充分的理由期待现在和将来的解码和执行(甚至对于 RSP 以外的基址寄存器)都非常有效。NASM 语法无法表达这一点,因此您必须手动编码。从GNU气体Intel语法objdump -d8b 04 23 mov eax,DWORD PTR [rbx+riz*1]的昂纳雾的例子10.20。(riz是一个虚构的索引零符号,这意味着有一个没有索引的 SIB)。我还没有测试 GAS 是否接受它作为输入。

使用仅需要或的指令的imm32和/或disp32形式。imm8disp0/disp32 Agner Fog 对 Sandybridge 的 uop 缓存(微架构指南表 9.1)的测试表明,立即数/位移的实际值才是重要的,而不是指令编码中使用的字节数。我没有关于 Ryzen 的 uop 缓存的任何信息。

因此 NASM imul eax, [dword 4 + rdi], strict dword 13(10 字节:opcode + 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。


您可能想在 64 位代码中编码mov eax, [symbol]为 6 字节a32 mov eax, [abs symbol],使用地址大小前缀来使用 32 位绝对地址。但这确实会导致在 Intel CPU 上解码时出现 Length-Changing-Prefix 停顿。幸运的是,如果您没有明确指定 32 位地址大小,而是使用mov r32, r/m32带有 ModR/M+SIB+disp32 绝对寻址的7 字节,则默认情况下 NASM/YASM/gas/clang 都不会执行此代码大小优化模式mov eax, [abs symbol]

在 64 位与位置相关的代码中,绝对寻址是使用 1 个额外字节与 RIP 相关的廉价方式。但请注意,32 位绝对 + 立即数需要 2 个周期才能从 uop 缓存中获取,这与 RIP 相对 + imm8/16/32 不同,即使它仍然使用 2 个指令条目,它也只需要 1 个周期。(例如,对于mov-store 或 a cmp)。所以cmp [abs symbol], 123从 uop 缓存中获取比 慢cmp [rel symbol], 123,即使两者都需要 2 个条目。没有立即,没有额外的费用

请注意,PIE 可执行文件甚至允许 ASLR 用于可执行文件,并且是许多 Linux 发行版中的默认设置,因此如果您可以将代码 PIC 保留而没有任何性能缺点,那么这是可取的。


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

添加像 rep 这样当前 CPU 忽略的前缀通常是不安全的,因为它们在未来的 ISA 扩展中可能意味着其他东西。

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

ds或者ss在访问存储器的指令前缀是一个空操作,并可能不引起在任何当前的CPU任何放缓。(@prl 在评论中建议了这一点)。

事实上,Agner Fog 的微架构指南在例 7.1 中使用了 ads前缀安排 IFETCH 块movq [esi+ecx],mm0来调整 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 或气体{vex3} vxorps xmm0,xmm0。如果 AVX512 可用,您也可以使用 4 字节 EVEX。


使用 64 位操作数大小,mov即使您不需要它,例如mov rax, strict dword 1在 NASM 中强制使用 7 字节符号扩展 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.
Run Code Online (Sandbox Code Playgroud)

您甚至可以使用mov reg, 0代替xor reg,reg.

mov r64, imm64当常量实际上很小(适合 32 位符号扩展)时,它有效地适合 uop 缓存。 1 个 uop 缓存条目,加载时间 = 1,与 for 相同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}前缀、 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
Run Code Online (Sandbox Code Playgroud)

气体具有编码覆盖伪前缀 {vex3}{evex}{disp8},和{disp32} 这些取代现在不推荐使用.s.d8.d32后缀

GAS 没有直接大小的覆盖,只有位移。

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
Run Code Online (Sandbox Code Playgroud)

GAS 在表达比需要更长的编码方面的功能严格不如 NASM。


归档时间:

查看次数:

683 次

最近记录:

7 年,3 月 前