Выравнивание кода в одном объектном файле влияет на производительность функции в другом объектном файле

Я знаком с выравниванием данных и производительностью, но я довольно новичок в выравнивании кода. Недавно я начал программирование на сборке 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 в свой путь выполнения.

Кроме того, мне нужно больше информации/данных, чтобы точно определить вашу конкретную проблему, но подумал, что это может помочь. Удачи:)