Нет, использование в основном простых инструкций x86 (например, избегание push
и использование sub rsp, whatever
и хранение аргументов с mov
) было полезной оптимизацией для P5-pentium, потому что он не знал как разделить компактные, но сложные инструкции внутри.Его суперскалярный конвейер шириной 2 может выполнять только пару простых инструкций.
Современные процессоры x86 (начиная с Intel P6 (Pentium Pro / PIII) и включая все процессоры x86-64) декодируют сложные инструкции для нескольких операций, которые могут бытьзапланировано самостоятельно.(И для общих сложных инструкций, таких как push
/ pop
, у них есть хитрости, чтобы обрабатывать их как один моп. В этом случае механизм стека, который переименовывает указатель стека вне части ядра, вышедшей из строяпоэтому нет необходимости в uop для rsp-=8
части push
.)
Инструкции источника памяти, такие как add eax, [rdi]
, могут даже декодироваться в один uop на процессорах Intel, микросинтезируя нагрузку сALU uop, только разделяя их в планировщике вне очереди для отправки в исполнительные блоки.В остальной части конвейера, он использует только 1 запись (во внешнем интерфейсе и ROB).(Но см. Режимы Micro Fusion и Addressing , чтобы узнать об ограничениях для Sandybridge с индексированными режимами адресации, немного ослабленными в Haswell и более поздних версиях.) Процессоры AMD просто естественным образом смешивают операнды памяти с инструкциями ALU и не используются для декодированияих к дополнительным m-ops / uops, чтобы у них не было причудливого имени.
Длина инструкции не совсем соотносится с явным образом.Например, idiv rcx
составляет всего 3 байта, но декодируется до 57 моп на Skylake.(Избегайте 64-битного деления, оно медленнее, чем 32-битное.)
Чем меньше код, тем лучше, все остальное равно.Предпочитайте 32-битный размер операнда, когда этого достаточно, чтобы избежать префиксов REX, и выберите регистр, который не нуждается в префиксах REX (например, ecx
вместо r8d
).Но обычно не тратьте лишние инструкции, чтобы это произошло.(например, используйте r8d
вместо сохранения / восстановления rbx
, чтобы вы могли использовать ebx
в качестве еще одного регистра с нулями).
Но когда все остальное не равно ,Размер обычно является последним приоритетом для высокой производительности , после минимизации мопов и сохранения коротких цепочек зависимостей задержки (особенно цепочек зависимостей, переносимых циклами).
Большинство программ проводят большую часть своего времени в циклах, достаточно маленьких, чтобы поместиться в кэш L1d, и много этого временив нескольких еще меньших циклах внутри этого.
Если вы не можете правильно определить «холодный» код (выполняется редко) , оптимизируя размер по скорости с чем-то вроде 3-байтовых push 1
/ pop rax
вместо 5-байтовых mov eax, 1
определенно не подходит по умолчанию. Clang / LLVM будет выдвигать / выдавливать константы с -Oz
(оптимизировать только по размеру), но не -Os
(оптимизировать длябаланс размера и скорости).
Использование inc
вместо add reg,1
экономит байт (только 1 в x86-64, против 2 в 32-битном коде). С регистром назначения это простов большинстве случаев на большинстве процессоров это происходит так же быстро. См. инструкцию INC против ADD 1: имеет ли это значение?
Современные основные x86-процессоры имеют декодированные кэш-памяти (AMD с тех порRyzen, Intel начиная с Sandybridge), которые в основном избегают узких мест на старших процессорах со средней инструкциейlength> 4.
До этого (Core2 / Nehalem) настройка, чтобы избежать узких мест переднего конца, была намного более сложной, чем просто использование коротких инструкций в среднем.См. Руководство по микроархам Agner Fog для получения подробной информации о шаблонах uop, которые декодеры могут обрабатывать в этих старых процессорах Intel, и эффектах выравнивания кода относительно 16-байтовых границ для выборки после перехода и многих других.
Семейство AMD Bulldozer помечает границы команд в кеше L1i и может декодировать до 2x 16 байт за цикл, если оба ядра кластера активны, в противном случае микроархив Agner Fog PDF (https://agner.org/optimize/) сообщает ~ 21 байт за цикл(по сравнению с Intel до 16 байт в такт для декодеров, когда они не работают из кэша UOP.) Более низкая внутренняя пропускная способность бульдозера, вероятно, означает, что узкие места внешнего интерфейса случаются реже. Но я действительно не знаю, у меня нетНастроил что-нибудь для семейства Bulldozer с доступом к оборудованию для тестирования чего-либо.
Пример: эта функция скомпилирована с помощью clang с -O3
, -Os
и -Oz
int sum(int*arr) {
int sum = 0;
for(int i=0;i<10240;i++) {
sum+=arr[i];
}
return sum;
}
Исходный код + выход asm в проводнике компилятора Godbolt , где вы можете поиграть с этим кодом и параметрами компилятора.
Iтакже использовал -fno-vectorize
, потому что я предполагаю, что вы не будете пытаться автоматически векторизовать с SSE2, даже если это базовый показатель для x86-64 (хотя это ускорит этот цикл в 4
# clang -O3 -fno-vectorize
sum: # @sum
xor eax, eax
mov ecx, 7
.LBB2_1: # =>This Inner Loop Header: Depth=1
add eax, dword ptr [rdi + 4*rcx - 28]
add eax, dword ptr [rdi + 4*rcx - 24]
add eax, dword ptr [rdi + 4*rcx - 20]
add eax, dword ptr [rdi + 4*rcx - 16]
add eax, dword ptr [rdi + 4*rcx - 12]
add eax, dword ptr [rdi + 4*rcx - 8]
add eax, dword ptr [rdi + 4*rcx - 4]
add eax, dword ptr [rdi + 4*rcx]
add rcx, 8
cmp rcx, 10247
jne .LBB2_1
ret
Это довольно глупо;это развернуло 8, но все еще только с 1 аккумулятором.Таким образом, это узкие места с задержкой в 1 цикл add
вместо 2 нагрузок на тактовую частоту в Intel со времен SnB и AMD со времен K8.(И только чтение 4 байта за такт, это, вероятно, не является узким местом в пропускной способности памяти.)
Это лучше при нормальном -O3, не отключая векторизацию, используя 2 векторных аккумулятора:
sum: # @sum
pxor xmm0, xmm0 # zero first vector register
mov eax, 36
pxor xmm1, xmm1 # 2nd vector
.LBB2_1: # =>This Inner Loop Header: Depth=1
movdqu xmm2, xmmword ptr [rdi + 4*rax - 144]
paddd xmm2, xmm0
movdqu xmm0, xmmword ptr [rdi + 4*rax - 128]
paddd xmm0, xmm1
movdqu xmm1, xmmword ptr [rdi + 4*rax - 112]
movdqu xmm3, xmmword ptr [rdi + 4*rax - 96]
movdqu xmm4, xmmword ptr [rdi + 4*rax - 80]
paddd xmm4, xmm1
paddd xmm4, xmm2
movdqu xmm2, xmmword ptr [rdi + 4*rax - 64]
paddd xmm2, xmm3
paddd xmm2, xmm0
movdqu xmm1, xmmword ptr [rdi + 4*rax - 48]
movdqu xmm3, xmmword ptr [rdi + 4*rax - 32]
movdqu xmm0, xmmword ptr [rdi + 4*rax - 16]
paddd xmm0, xmm1
paddd xmm0, xmm4
movdqu xmm1, xmmword ptr [rdi + 4*rax]
paddd xmm1, xmm3
paddd xmm1, xmm2
add rax, 40
cmp rax, 10276
jne .LBB2_1
paddd xmm1, xmm0 # add the two accumulators
# and horizontal sum the result
pshufd xmm0, xmm1, 78 # xmm0 = xmm1[2,3,0,1]
paddd xmm0, xmm1
pshufd xmm1, xmm0, 229 # xmm1 = xmm0[1,1,2,3]
paddd xmm1, xmm0
movd eax, xmm1 # extract the result into a scalar integer reg
ret
Эта версия разворачивает, вероятно, больше, чем нужно;накладные расходы на петли крошечные, а movdqu
+ paddd
- всего 2 мопа, так что мы далеко от узких мест на переднем конце.При нагрузке 2 на такт movdqu
этот цикл может обрабатывать 32 байта ввода за такт, предполагая, что данные горячие в кеше L1d или, возможно, L2, в противном случае он будет работать медленнее.Такое развертывание с превышением минимума позволит запустить выполнение вне очереди и увидеть условие выхода из цикла до того, как работа paddd
будет завершена, и, возможно, в большинстве случаев скрыть неверный прогноз ветвления на последней итерации.
Использование более 2-х аккумуляторов для скрытия задержки очень важно в коде FP, где большинство инструкций не имеют задержки одного цикла.(Это также было бы полезно для этой функции в семействе AMD Bulldozer, где paddd
имеет задержку в 2 цикла.)
При больших развертываниях и больших смещениях компиляторы иногда генерируют много инструкций, которым требуется disp32
смещение вместо disp8
в режиме адресации.Выбор точки, в которой вы увеличиваете счетчик цикла или указатель, чтобы сохранить как можно больше режимов адресации, используя смещение -128 .. +127, вероятно, было бы неплохо.
Если только вы не настраиваетесь на Nehalem /Core2 или другие процессоры без кэша UOP, вы, вероятно, не хотите добавлять дополнительные издержки цикла (add rdi, 256
дважды вместо add rdi, 512
или что-то в этом роде) просто для уменьшения размера кода.
Для сравнения, clang -Os
по-прежнему автоматически векторизуется (если вы не отключите его) с внутренним циклом, длина которого ровно 4 мопа на процессорах Intel.
# clang -Os
.LBB2_1: # =>This Inner Loop Header: Depth=1
movdqu xmm1, xmmword ptr [rdi + 4*rax]
paddd xmm0, xmm1
add rax, 4
cmp rax, 10240
jne .LBB2_1
Но с clang -Os -fno-vectorize
мы получаем простую и очевидную минимальную скалярную реализацию:
# clang -Os -fno-vectorize
sum: # @sum
xor ecx, ecx
xor eax, eax
.LBB2_1: # =>This Inner Loop Header: Depth=1
add eax, dword ptr [rdi + 4*rcx]
inc rcx
cmp rcx, 10240
jne .LBB2_1
ret
Пропущенная оптимизация: использование ecx
позволит избежать префикса REX для inc
и cmp
.Известно, что диапазон устанавливается в 32-битной версии.Вероятно, он использует RCX, потому что он повысил int
до 64-битных, чтобы избежать расширения знака movsxd rcx,ecx
до 64-битных перед использованием в режиме адресации.(Поскольку переполнение со знаком - это UB в C.) Но после этого он может снова оптимизировать его, заметив диапазон.
Цикл равен 3 моп (при условии, что в Intel слитый макрос cmp / jne, так как Nehalemи AMD с Bulldozer), или 4 мопа на Sandybridge (отмена добавления с помощью индексированного режима адресации.) Цикл инкремента указателя может быть несколько более эффективным на некоторых процессорах, требуя только 3 мопа внутри цикла даже на SnB / IvB.
Вывод Clang -Oz
на самом деле больше, что свидетельствует о признаках его стратегии генерации кода.Нельзя доказать, что многие циклы выполняются хотя бы 1 раз, и, следовательно, им нужна условная ветвь, чтобы пропустить цикл, а не попадать в него в случае нулевого времени выполнения.Или им нужен прыжок к точке входа возле дна.( Почему циклы всегда компилируются в стиле "do ... while" (прыжок в хвост)? ).
Похоже, код LLVM -Oz
code-gen безоговорочно использует переход книжняя стратегия без проверки, всегда ли верно условие на первой итерации.
sum: # @sum
xor ecx, ecx
xor eax, eax
jmp .LBB2_1
.LBB2_3: # in Loop: Header=BB2_1 Depth=1
add eax, dword ptr [rdi + 4*rcx]
inc rcx
.LBB2_1: # =>This Inner Loop Header: Depth=1
cmp rcx, 10240
jne .LBB2_3
ret
Все то же самое, кроме дополнительного jmp
для входа в цикл.
В функции, которая выполнялаБолее того, вы увидите больше различий в коде поколения.Как, например, использование медленного div
даже для констант времени компиляции вместо мультипликативного обратного ( Почему GCC использует умножение на странное число при реализации целочисленного деления? ).