如何判断在 x86-64 汇编中是否正在使用 16 字节对齐地址进入循环?

How can one figure out if a loop is being entered with a 16 byte aligned address in x86-64 assembly?

我是 x86-64 的初学者,我正在努力提高,尤其是在性能优化方面。

我已经通读了 agner's optimization manual volume 2 的部分内容。反复声明输入具有 16 字节对齐的关键 hotspot/loop 是多么重要。现在我无法确定循环的入口是否是 16 字节对齐的。

你是不是应该在循环入口之前将子程序中每条指令的字节成本加起来,看它是否可以被 16 整除? 我已经查阅了 x86-64 的英特尔开发人员手册,但无法读取其中的指令具有哪些字节长度。指令的字节大小只是操作码的总和吗?那么在 MOV r64/m16 和 Opcode REX.W + 8C 的情况下,大小是 2 个字节吗? (一个用于 REX.W 前缀,一个用于 8C)。

考虑以下代码,假设一些字符串作为参数传递到 rdi 中,将在 .LmanipulationLoop 中进行操作:

string_fun:
   cmp cl, byte ptr [rdi]
   jz .Lend
   xor rcx, rcx

.LmanipulationLoop
  *some string operation*

.Lend
  ret

所以根据我目前的理解:

总而言之,(假设我是对的)5 个字节。这是否意味着我在 .LmanipulationLoop 之前需要 11 个 NOP 以确保对齐进入循环?

不需要手动计算,assemblers可以为您完成。手动计算是仅当您想要比仅使用 NOP 填充更聪明以在插入填充的点之后对齐某些内容时才有用。

通常您会在标签前使用 .p2align 4 (GAS) 或 align 16 (NASM1) 以获得 assembler 计算出需要多少填充,并发出一个或多个长 NOP。 (不是 11 个单字节 NOP,那会很糟糕,因为它们每个都必须单独解码)。

And/or 使用调试器或 disassembler 检查标签地址而不是手动计算它,如果你的目标是

如果您想尽量减少所需的 NOP 数量,了解哪些指令的长度很有用,但在这种情况下 trial/error 可以找到良好的指令序列这让你最多需要一个长的 NOP。

在具有 uop 高速缓存的 CPU 上并不总是需要对齐循环顶部

通常真正重要的是 uop 缓存行的 32 字节边界。或者对于具有循环缓冲区的 CPU 上的大多数小循环来说根本没有(但请注意,Skylake / Kaby Lake 的 LSD 被微代码更新禁用以修复错误)。如果避免从 uop 缓存中获取前端瓶颈,则非常关键循环顶部的 32 字节对齐可能很有用。或者对于每次迭代可以 运行 1 个周期的微小循环,将整个循环放在同一个 uop 缓存行中是必不可少的(否则前端每次迭代需要两个周期来获取它)。

不幸的是,在 Skylake 派生的 CPU 上循环对齐的主要问题是对齐循环的 底部 以解决性能坑洞 其中 a jcc or macro-fused compare+branch that touches a 32-byte boundary disables the uop cache for that line.


简单对齐示例:

我修复了您源代码中的错误(标签后缺少 :,以及使用 32 位操作数大小对 RCX 进行异或零操作的性能错误)。虽然在这种情况下你可能想要 xor rcx,rcx 只是为了让它更长,因为你知道需要一些 NOP 字节。不过 REX.W=0 会更好,而不是

然后我用 SIMD 负载填充了占位符。

.intel_syntax noprefix
.p2align 4                  # align the top of the function
string_fun:
   cmp cl, byte ptr [rdi]
   jz .Lend
   xor ecx, ecx             # zeroing ECX implicitly zero-extends into RCX, saving a REX prefix
   lea rsi, [rdi + 1024]    # end pointer

# .p2align 4                # emit padding until a 2^4 boundary
.LmanipulationLoop:           # do {
   movdqu  xmm0, [rdi]
      # Do something like pcmpeqb / pmovmskb with the string bytes ...
   add    rdi, 16
   cmp    rdi, rsi
   jb    .LmanipulationLoop   # }while(p < endp);

.Lend:
  ret

Assemble 与 gcc -Wa,--keep-locals -c foo.Sas --keep-locals foo.s.
--keep-locals 使 .L 标签在目标文件的符号 table 中可见。

然后 disassemble 与 objdump -drwC -Mintel foo.o:

0000000000000000 <string_fun>:
   0:   3a 0f                   cmp    cl,BYTE PTR [rdi]
   2:   74 16                   je     1a <.Lend>
   4:   31 c9                   xor    ecx,ecx
   6:   48 8d b7 00 04 00 00    lea    rsi,[rdi+0x400]
     # note address of this label, 
     # or without --keep-locals, of the instruction that you know is the loop top
000000000000000d <.LmanipulationLoop>:
   d:   f3 0f 6f 07             movdqu xmm0,XMMWORD PTR [rdi]
  11:   48 83 c7 10             add    rdi,0x10
  15:   48 39 f7                cmp    rdi,rsi
  18:   72 f3                   jb     d <.LmanipulationLoop>       # note the jump target address

000000000000001a <.Lend>:
  1a:   c3                      ret    

或者在 .p2align 4 未注释的情况下,assembler 发出一个 3 字节的 NOP:

0000000000000000 <string_fun>:
   0:   3a 0f                   cmp    cl,BYTE PTR [rdi]
   2:   74 19                   je     1d <.Lend>
   4:   31 c9                   xor    ecx,ecx
   6:   48 8d b7 00 04 00 00    lea    rsi,[rdi+0x400]
   d:   0f 1f 00                nop    DWORD PTR [rax]         # This is new, note that it's *before* the jump target

0000000000000010 <.LmanipulationLoop>:
  10:   f3 0f 6f 07             movdqu xmm0,XMMWORD PTR [rdi]
  14:   48 83 c7 10             add    rdi,0x10
  18:   48 39 f7                cmp    rdi,rsi
  1b:   72 f3                   jb     10 <.LmanipulationLoop>

000000000000001d <.Lend>:
  1d:   c3                      ret    

反汇编 .o 目标文件不会显示调用外部函数的正常地址;它尚未链接,因此未填充 rel32 位移。但是 -r 将显示重定位信息。源文件中的跳转确实在 assemble 时得到完全解决。


脚注 1:请注意 NASM 有一个错误的默认值,您需要这样的东西来获得长 NOP 而不是多个单字节 NOP:

%use smartalign
alignmode p6, 64