我熟悉数据对齐和性能,但我对对齐代码比较陌生。我最近开始使用 NASM 在 x86-64 汇编中编程,并且一直在使用代码对齐来比较性能。据我所知,NASM 插入了 nop
指令来实现代码对齐。
这是我在 Ivy Bridge 系统上尝试过的一个函数
void triad(float *x, float *y, float *z, int n, int repeat) {
float k = 3.14159f;
int(int r=0; r<repeat; r++) {
for(int i=0; i<n; i++) {
z[i] = x[i] + k*y[i];
}
}
}
下面是我为此使用的程序集。如果我不指定对齐方式,我的性能与峰值相比只有 90% 左右。但是,当我将循环之前的代码以及两个内部循环对齐到 16 字节时,性能会跃升至 96%。很明显,这种情况下的代码对齐会有所不同。
但这是最奇怪的部分。如果我将最内层循环对齐到 32 字节,它不会影响该函数的性能,但是,在该函数的另一个版本中,在单独的目标文件中使用内在函数,我将其性能从 90% 跃升至 95%!
我做了一个对象转储(使用 objdump -d -M intel
),版本对齐到 16 字节(我将结果发布到这个问题的末尾)和 32 字节,它们是相同的!事实证明,在两个目标文件中,最内层的循环无论如何都与 32 字节对齐。但是一定有一些区别。
我对每个目标文件进行了十六进制转储,目标文件中有一个字节不同。对齐到 16 字节的目标文件有一个字节为 0x10
,对齐到 32 字节的目标文件有一个字节为 0x20
。 到底是怎么回事!为什么一个目标文件中的代码对齐会影响另一个目标文件中函数的性能?我如何知道将我的代码对齐到的最佳值是多少?
我唯一的猜测是,当加载程序重新定位代码时,32 字节对齐的目标文件会影响使用内部函数的其他目标文件。您可以在 Obtaining peak bandwidth on Haswell in the L1 cache: only getting 62% 找到测试所有这些的代码
我正在使用的 NASM 代码:
global triad_avx_asm_repeat
;RDI x, RSI y, RDX z, RCX n, R8 repeat
pi: dd 3.14159
align 16
section .text
triad_avx_asm_repeat:
shl rcx, 2
add rdi, rcx
add rsi, rcx
add rdx, rcx
vbroadcastss ymm2, [rel pi]
;neg rcx
align 16
.L1:
mov rax, rcx
neg rax
align 16
.L2:
vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm1, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm1
add rax, 32
jne .L2
sub r8d, 1
jnz .L1
vzeroupper
ret
来自 objdump -d -M intel test16.o
的结果。如果我在 .L2
之前的程序集中将 align 16
更改为 align 32
,则反汇编是相同的。但是,目标文件仍然相差一个字节。
test16.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <pi>:
0: d0 0f ror BYTE PTR [rdi],1
2: 49 rex.WB
3: 40 90 rex xchg eax,eax
5: 90 nop
6: 90 nop
7: 90 nop
8: 90 nop
9: 90 nop
a: 90 nop
b: 90 nop
c: 90 nop
d: 90 nop
e: 90 nop
f: 90 nop
0000000000000010 <triad_avx_asm_repeat>:
10: 48 c1 e1 02 shl rcx,0x2
14: 48 01 cf add rdi,rcx
17: 48 01 ce add rsi,rcx
1a: 48 01 ca add rdx,rcx
1d: c4 e2 7d 18 15 da ff vbroadcastss ymm2,DWORD PTR [rip+0xffffffffffffffda] # 0 <pi>
24: ff ff
26: 90 nop
27: 90 nop
28: 90 nop
29: 90 nop
2a: 90 nop
2b: 90 nop
2c: 90 nop
2d: 90 nop
2e: 90 nop
2f: 90 nop
0000000000000030 <triad_avx_asm_repeat.L1>:
30: 48 89 c8 mov rax,rcx
33: 48 f7 d8 neg rax
36: 90 nop
37: 90 nop
38: 90 nop
39: 90 nop
3a: 90 nop
3b: 90 nop
3c: 90 nop
3d: 90 nop
3e: 90 nop
3f: 90 nop
0000000000000040 <triad_avx_asm_repeat.L2>:
40: c5 ec 59 0c 07 vmulps ymm1,ymm2,YMMWORD PTR [rdi+rax*1]
45: c5 f4 58 0c 06 vaddps ymm1,ymm1,YMMWORD PTR [rsi+rax*1]
4a: c5 fc 29 0c 02 vmovaps YMMWORD PTR [rdx+rax*1],ymm1
4f: 48 83 c0 20 add rax,0x20
53: 75 eb jne 40 <triad_avx_asm_repeat.L2>
55: 41 83 e8 01 sub r8d,0x1
59: 75 d5 jne 30 <triad_avx_asm_repeat.L1>
5b: c5 f8 77 vzeroupper
5e: c3 ret
5f: 90 nop
最佳答案
啊啊啊,代码对齐...
代码对齐的一些基础知识..
- 大多数英特尔架构每个时钟获取 16B 的指令。
- 分支预测器有一个更大的窗口,通常每个时钟看起来是它的两倍。这样做的目的是抢在获取的指令之前。
- 代码的对齐方式将决定您可以在任何给定时钟(简单的代码局部性参数)解码和预测哪些指令。
- 大多数现代英特尔架构都在不同级别缓存指令(解码前在宏指令级别,或解码后在微指令级别)。只要您在微/宏缓存之外执行,这就消除了代码对齐的影响。
- 此外,大多数现代英特尔架构都具有某种形式的循环流检测器,可以检测循环,再次从绕过前端获取机制的缓存中执行它们。
- 一些英特尔架构对可以缓存什么不能缓存什么很挑剔。通常依赖于指令/uops/对齐/分支/等的数量。在某些情况下,对齐可能会影响缓存的内容和不缓存的内容,并且您可以创建填充可以防止或导致循环被缓存的情况。
- 让事情变得更复杂的是,指令的地址也被分支预测器使用。它们以多种方式使用,包括(1)作为分支预测缓冲区的查找以预测分支,(2)作为键/值来维护某种形式的全局分支行为状态以用于预测目的,(3)作为确定间接分支目标等的关键。因此,对齐实际上会对分支预测产生相当大的影响,在某些情况下,由于混叠或其他不良预测。
- 一些架构使用指令地址来确定何时预取数据,如果存在正确的条件,代码对齐可能会对此产生干扰。
- 对齐循环并不总是一件好事,这取决于代码的布局方式(尤其是在循环中存在控制流的情况下)。
说了这么多废话,您的问题可能是其中之一。重要的是不仅要查看对象的反汇编,还要查看可执行文件的反汇编。您想在链接所有内容后查看最终地址是什么。在一个对象中进行更改,可能会影响链接后另一对象中指令的对齐方式/地址。
在某些情况下,几乎不可能以最大化性能的方式调整代码,这仅仅是因为许多低级架构行为难以控制和预测(这并不一定意味着情况总是如此).在某些情况下,最好的办法是采用一些默认的对齐策略(比如对齐 16B 边界上的所有条目,并且外部循环相同),以便最大限度地减少性能因更改而异的数量。作为一般策略,对齐函数条目是好的。只要您不在执行路径中添加 nop,对齐相对较小的循环是好的。
除此之外,我需要更多信息/数据来查明您的确切问题,但我认为其中一些可能会有所帮助。祝您好运 :)
关于一个目标文件中的代码对齐会影响另一个目标文件中函数的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25958649/