Выравнивание кода в одном объектном файле влияет на производительность функции в другом объектном файле
Я знаком с выравниванием данных и производительностью, но я довольно новичок в выравнивании кода. Недавно я начал программирование на сборке 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
Ответы
Ответ 1
Запутанная природа эффекта (собранный код не меняется!), который вы видите, обусловлен выравниванием раздела. При использовании макроса ALIGN
в NASM он фактически имеет два отдельных эффекта:
-
Добавьте 0 или более инструкций nop
, чтобы следующая команда была выровнена с указанной границей из двух сторон.
-
Вывести неявный макрокоманд SECTALIGN
, который установит директиву выравнивания секций на сумму выравнивания 1.
Первая точка - это общепринятое поведение для выравнивания. Он выравнивает цикл относительно участка в выходном файле.
Вторая часть также нужна: представьте, что ваша петля была выровнена с 32-байтовой границей в собранном разделе, но затем загрузчик времени выполнения разместил ваш раздел в памяти по адресу, выровненному только до 8 байтов: это сделало бы выравнивание в файле совершенно бессмысленно. Чтобы исправить это, большинство исполняемых форматов позволяют каждому разделу указывать требование выравнивания , и загрузчик/компоновщик времени выполнения обязательно загрузит раздел по адресу памяти, который соответствует требование.
Что делает скрытый макрос SECTALIGN
- он гарантирует, что ваш макрос ALIGN
работает.
Для вашего файла нет разницы в собранном коде между ALIGN 16
и ALIGN 32
, потому что следующая граница из 16 байтов также будет следующей 32-байтной границей (конечно, каждая другая 16-байтная граница является 32-байтным, так что это происходит примерно в половину времени). Неявный вызов SECTALIGN
по-прежнему отличается, и что одноразрядное различие вы видите в своем шестнадцатеричном формате. 0x20 является десятичным числом 32, а 0x10 - десятичным.
Вы можете проверить это с помощью objdump -h <binary>
. Здесь пример в двоичном файле I, выровненном до 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-байтной границей, но это не влияет на первую функцию, поскольку она не чувствительна к выравниванию. Поскольку компоновщик объединил ваши объектные файлы в один раздел, большее выравнивание 32 изменяет выравнивание каждой функции (и инструкции) в этом разделе, включая ваш другой метод, и поэтому он изменяет производительность вашей другой функции, которая является выравниванием, чувствительный.
1 Чтобы быть точным, SECTALIGN
изменяет выравнивание раздела только в том случае, если выравнивание текущего раздела меньше заданной величины, поэтому выравнивание финальной секции будет таким же, как и наибольший SECTALIGN
директивы в разделе.
Ответ 2
Ahhh, выравнивание кода...
Некоторые основы выравнивания кода.
- Большинство архитектур Intel получают 16 байт инструкций за часы.
- Проектор-ветки имеет большее окно и обычно выглядит двойным, что за часы. Идея состоит в том, чтобы опередить введенные инструкции.
- Как выравнивается ваш код, будет указывать, какие инструкции вы можете использовать для декодирования и прогнозирования на любых заданных часах (простой аргумент локальности кода).
- Большинство современных инструкций кэширования архитектуры Intel на разных уровнях (либо на уровне макрокоманд перед декодированием, либо на уровне микроуровней после декодирования). Это устраняет эффекты выравнивания кода, если вы выполняете кеширование микро/макросов.
- Кроме того, у большинства современных архитектур Intel есть определенный вид детекторов потока циклов, которые обнаруживают циклы, снова, выполняя их из некоторого кеша, который обходит механизм выборки переднего конца.
- Некоторые архитектуры Intel скупают то, что они могут кэшировать, и то, что они не могут. Часто есть зависимости от количества инструкций /uops/alignment/branches/etc. Выравнивание может в некоторых случаях влиять на то, что кэшировано, а что нет, и вы можете создавать случаи, когда заполнение может предотвратить или заставить цикл получить кеширование.
- Чтобы сделать вещи еще более сложными, адреса инструкций также используются предиктором ветки. Они используются несколькими способами, в том числе (1) для поиска в буфере предсказания ветвей для прогнозирования ветвей, (2) в качестве ключа/значения для поддержания некоторой формы глобального состояния поведения ветвей для целей прогнозирования, (3) как ключ к определению косвенных целей и т.д. Поэтому выравнивание может фактически иметь очень большое влияние на предсказание ветвей, в некоторых случаях, из-за псевдонимов или других плохих прогнозов.
- В некоторых архитектурах используются адреса инструкций, чтобы определить, когда предварительно выбирать данные, и выравнивание кода может помешать этому, если существуют только правильные условия.
- Выравнивание циклов - это не всегда хорошая работа, в зависимости от того, как выкладывается код (особенно, если поток управления в цикле).
Сказав все, что бла-бла, ваша проблема может быть одной из них. Важно посмотреть на разборку не только объекта, но и исполняемого файла. Вы хотите узнать, какие конечные адреса после того, как все связано. Внесение изменений в один объект может повлиять на выравнивание/адреса инструкций в другом объекте после ссылки.
В некоторых случаях почти невозможно выровнять код таким образом, чтобы максимизировать производительность, просто из-за того, что многие архитектурные поведения низкого уровня трудно контролировать и прогнозировать (что не обязательно означает, что это всегда так). В некоторых случаях лучше всего иметь стратегию выравнивания по умолчанию (например, выровнять все записи на границах 16B, а внешние петли одинаковы), чтобы вы минимизировали объем вашей работы от изменения к изменению. В качестве общей стратегии выравнивание записей функций является хорошим. Выравнивание циклов, относительно небольших, является хорошим, если вы не добавляете nops в свой путь выполнения.
Кроме того, мне нужно больше информации/данных, чтобы точно определить вашу конкретную проблему, но подумал, что это может помочь. Удачи:)