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