如何确定x86-64程序集中是否输入了一个16字节对齐地址的循环?

问题描述 投票:2回答:1

我是x86-64的初学者,尤其是在性能优化方面,我正在尝试变得更好。

我已经阅读了agner's optimization manual volume 2的各个部分。反复指出,进入16字节对齐的关键热点/循环非常重要。现在,我很难弄清楚循环中的条目是否对齐16字节。

您是否应该将循环入口之前子例程中每条指令的字节开销加起来,看看它是否可以除以16?我已咨询了x86-64的英特尔开发人员手册,但无法读取其中哪些指令具有哪些字节长度的信息。指令的字节大小是否只是将操作码加在一起?因此,对于带有操作码REX.W + 8C的MOV r64 / m16,大小为2个字节吗? (一个用于REX.W前缀,一个用于8C)。

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

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

.LmanipulationLoop
  *some string operation*

.Lend
  ret

所以基于我目前的理解:

  • cmp cl,字节ptr [rdi],为此的操作码是0x38(CMP r / m8,r8),所以1个字节
  • jz .Lend,为此,操作码为0x0F 84(jz rel32),所以2个字节(我不确定这是正确的操作码)
  • xor rcx,rcx,操作码是REX.W + 0x33(xor r64,r / m64)所以2个字节

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

assembly optimization x86-64 memory-alignment micro-optimization
1个回答
1
投票

[您不需要需要手动执行此操作,汇编程序可以为您执行此操作。仅当您希望比仅使用NOP填充以在对齐后立即对齐时更聪明时,手动计算才有用。插入填充的位置。

通常,在标签前使用.p2align 4(GAS)或.p2align 4(NASM 1)可使汇编器确定需要多少填充,并发出一个或多个长的NOP。 (不是11个单字节NOP,那会很糟糕,因为它们每个人都必须分别解码)。

和/或使用调试器或反汇编器检查标签地址,而不是手动计算,如果您的目标是[C0

如果要尽量减少所需的NOP数量,了解有关哪些指令的长度是很有用的,但是在这种情况下,可以通过反复试验来找到一个好的指令序列,从而使您无需最长时间的NOP。

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

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

不幸的是,Skylake派生CPU上的循环对齐的主要问题是对齐循环的bottom以解决性能隐患 align 16


简单对齐示例:

我修复了您源代码中的错误(标签后缺少What methods can be used to efficiently extend instruction length on modern x86?,以及使用32位操作数大小对RCX进行零运算的性能错误)。尽管在这种情况下,您可能想要a jcc or macro-fused compare+branch that touches a 32-byte boundary disables the uop cache for that line只是为了使其更长一些,因为您知道需要一些NOP字节。 jcc会更好,而不是:

并且我用SIMD加载填充了占位符。

xor rcx,rcx

REX.W=0hurt performance on Silvermont组装。.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 使gcc -Wa,--keep-locals -c foo.S标签在目标文件的符号表中可见。

然后用as --keep-locals foo.s拆解:

--keep-locals

或者不带.L的注释,汇编器将发出一个3字节的NOP:

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 目标文件不会显示调用外部函数的合理地址;它尚未链接,因此尚未填充rel32位移。但是.p2align 4将显示重定位信息。并在汇编时完全解决了源文件中的跳转问题。


脚注1:请注意,NASM的默认设置很差,您需要类似的方法来获取长的NOP,而不是多个单字节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    
© www.soinside.com 2019 - 2024. All rights reserved.