Я знаком с выравниванием данных и производительностью, но я довольно новичок в выравнивании кода. Недавно я начал программирование на сборке x86-64 с NASM и сравнивал производительность с использованием выравнивания кода. Насколько я могу сказать, 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-байтовый выровненный объектный файл влияет на другой файл объекта, используя встроенные средства. Вы можете найти код, чтобы проверить все это на Получение максимальной пропускной способности на 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
Результат от 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