我熟悉数据对齐和性能,但我很擅长对齐代码。我最近开始使用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
的结果。如果我在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
答案 0 :(得分:4)
啊,代码对齐......
代码对齐的一些基础知识..
说完所有这些等等,你的问题可能就是其中之一。看看不仅是对象的反汇编,还有可执行文件的重组,这一点非常重要。您希望在链接完所有内容后查看最终地址。在一个对象中进行更改可能会影响链接后另一个对象中指令的对齐/地址。
在某些情况下,几乎不可能以最大限度地提高性能的方式调整代码,原因很简单,因为许多低级别的架构行为很难控制和预测(不一定是这样)意味着总是如此)。在某些情况下,最好的办法是采用一些默认的对齐策略(比如对齐16B边界上的所有条目,外部循环相同),这样可以最大限度地减少性能因变化而变化的程度。作为一般策略,对齐函数条目是好的。只要您不在执行路径中添加nops,对齐相对较小的循环就是好的。
除此之外,我需要更多的信息/数据来确定您的确切问题,但认为其中一些可能有所帮助..祝您好运:))
答案 1 :(得分:3)
您看到的效果的混乱性质(汇编代码不会改变!)是由于部分对齐。在NASM中使用ALIGN
宏时,它实际上有两个独立的效果:
添加0个或多个nop
指令,以便下一条指令与指定的二次幂边界对齐。
发出隐式SECTALIGN
宏调用,将 section alignment指令设置为对齐量 1 。
第一点是通常理解的对齐行为。它相对于输出文件中的部分对齐循环。
然而,第二部分也需要:想象你的循环在汇编部分中与32字节边界对齐,但是然后运行时加载器将你的部分放在内存中,地址只对应于8个字节:这会使文件内对齐毫无意义。要解决此问题,大多数可执行格式允许每个部分指定alignment requirement,并且运行时加载器/链接器将确保将该部分加载到符合要求的内存地址。
这就是隐藏的SECTALIGN
宏的作用 - 它确保您的ALIGN
宏有效。
对于您的文件,ALIGN 16
和ALIGN 32
之间的汇编代码没有区别,因为下一个16字节边界恰好也是下一个32字节边界(当然,每隔一个16字节边界是一个32字节的边界,因此大约一半的时间发生。隐式SECTALIGN
调用仍然不同,,这是你在hexdump中看到的一个字节差异。 0x20是十进制32,0x10是十进制16。
您可以使用objdump -h <binary>
验证这一点。这是一个关于二进制的示例我对齐到32个字节:
objdump -h loop-test.o
loop-test.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000d18a 0000000000000000 0000000000000000 00000180 2**5
CONTENTS, ALLOC, LOAD, READONLY, CODE
2**5
列中的Algn
是32字节对齐方式。使用16字节对齐时,此更改为2**4
。
现在应该清楚会发生什么 - 对齐示例中的第一个函数会更改节对齐,但不会更改程序集。将程序链接在一起时,链接器将合并各个.text
部分并选择最高的对齐方式。
在运行时,这会导致代码与32字节边界对齐 - 但这不会影响第一个函数,因为它不对齐敏感。由于链接器已将您的目标文件合并为一个部分,因此更大的对齐方式会更改该部分中每个函数(和指令)的对齐方式,包括您的其他方法,因此它会更改您的其他函数, 对齐敏感。
1 准确地说,SECTALIGN
仅在当前部分对齐小于指定数量时才更改部分对齐 - 因此最终部分对齐将与部分中最大的 SECTALIGN
指令。