Z b*_*son 4 c x86 assembly nasm avx
我熟悉数据对齐和性能,但对对齐代码相当陌生。我最近开始使用 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];
}
}
}
Run Code Online (Sandbox Code Playgroud)
我为此使用的程序集如下。如果我不指定对齐方式,我的性能与峰值相比仅为 90% 左右。然而,当我将循环之前的代码以及两个内部循环对齐为 16 字节时,性能跃升至 96%。很明显,这种情况下的代码对齐会产生影响。
但这是最奇怪的部分。如果我将最里面的循环对齐到 32 字节,则该函数的性能没有任何差异,但是,在该函数的另一个版本中,在单独的对象文件中使用内部函数,我链接它的性能从 90% 跃升至 95%!
我做了一个对象转储(使用objdump -d -M intel
)的版本对齐到16字节(我将结果发布到这个问题的末尾)和32字节,它们是相同的!事实证明,在两个目标文件中,最里面的循环无论如何都与 32 字节对齐。但一定有一些区别。
我对每个目标文件进行了十六进制转储,目标文件中有一个字节不同。与 16 字节对齐的目标文件有一个带有 的字节0x10
,与 32 字节对齐的目标文件有一个带有 的字节0x20
。到底是怎么回事!为什么一个目标文件中的代码对齐会影响另一个目标文件中函数的性能?我如何知道将我的代码调整到的最佳值是多少?
我唯一的猜测是,当加载程序重新定位代码时,32 字节对齐的对象文件会使用内在函数影响其他对象文件。您可以在 Haswell 的 L1 缓存中获取峰值带宽中找到测试所有这些的代码:仅获得 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
Run Code Online (Sandbox Code Playgroud)
结果来自objdump -d -M intel test16.o
. 如果我在前面的程序集中更改align 16
为,则反汇编是相同的。然而,目标文件仍然存在一个字节的差异。align 32
.L2
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
Run Code Online (Sandbox Code Playgroud)
小智 5
啊啊,代码对齐...
代码对齐的一些基础知识..
说了这么多,你的问题可能是其中之一。重要的是不仅要查看对象的反汇编,还要查看可执行文件的反汇编。您想查看所有链接后的最终地址是什么。在一个对象中进行更改可能会影响链接后另一对象中指令的对齐/地址。
在某些情况下,几乎不可能以最大化性能的方式调整代码,这仅仅是因为许多低级架构行为难以控制和预测(这并不一定意味着情况总是如此)。在某些情况下,最好的选择是采用一些默认的对齐策略(例如对齐 16B 边界上的所有条目,并且外部循环相同),以便最大限度地减少每次更改时的性能差异。作为一般策略,对齐函数条目是很好的。只要不在执行路径中添加 nop,对齐相对较小的循环就很好。
除此之外,我需要更多信息/数据来查明您的确切问题,但认为其中一些可能会有所帮助..祝您好运:)