При анализе вывода различных компиляторов для множества фрагментов кода я заметил, что компилятор Intel C (ICC) имеет сильную тенденцию предпочитать выдавать пару инструкций NEG
+ ADD
, где другие компиляторы используйте одну инструкцию SUB
.
В качестве простого примера рассмотрим следующий код C:
uint64_t Mod3(uint64_t value)
{
return (value % 3);
}
ICC переводит это на следующий машинный код (независимо от уровня оптимизации):
mov rcx, 0xaaaaaaaaaaaaaaab
mov rax, rdi
mul rcx
shr rdx, 1
lea rsi, QWORD PTR [rdx+rdx*2]
neg rsi ; \ equivalent to:
add rdi, rsi ; / sub rdi, rsi
mov rax, rdi
ret
В то время как другие компиляторы (включая MSVC, GCC и Clang) будут генерировать по существу эквивалентный код, за исключением того, что последовательность NEG
+ ADD
заменяется одной инструкцией SUB
.
Как я уже сказал, это не просто причуда, как ICC компилирует этот конкретный фрагмент. Это шаблон, который я неоднократно наблюдал при анализе разборки для арифметических операций. Я обычно не думаю об этом, за исключением того, что ICC, как известно, является довольно хорошим оптимизирующим компилятором, и он разработан людьми, которые имеют инсайдерскую информацию об их микропроцессорах.
Может ли быть что-то, что Intel знает о реализации инструкции SUB
на своих процессорах, что делает ее более оптимальной для ее разложения в инструкции NEG
+ ADD
? Использование инструкций в стиле RISC, которые декодируются в более простые μops, является хорошо известным советом по оптимизации для современных микроархитектур, так что возможно, что SUB
разбивается внутренне на отдельные NEG
и ADD
μops и что он фактически более эффективен для интерфейсного декодера использовать эти "более простые" инструкции? Современные процессоры сложны, поэтому все возможно.
Полные таблицы инструкций Agner Fog подтверждают мою интуицию, однако, что это на самом деле пессимизация. SUB
так же эффективен, как ADD
для всех процессоров, поэтому дополнительная требуемая инструкция NEG
просто замедляет работу.
Я также выполнил две последовательности через Intel собственный анализатор кода архитектуры для анализа пропускной способности. Хотя точное количество циклов и привязки портов варьируются от одной микроархитектуры к другой, один SUB
кажется превосходен во всех отношениях от Nehalem до Broadwell. Вот два отчета, созданных инструментом для Haswell:
Intel(R) Architecture Code Analyzer Version - 2.2 build:356c3b8 (Tue, 13 Dec 2016 16:25:20 +0200)
Binary Format - 64Bit
Architecture - HSW
Analysis Type - Throughput
Throughput Analysis Report
--------------------------
Block Throughput: 1.85 Cycles Throughput Bottleneck: Dependency chains (possibly between iterations)
Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
---------------------------------------------------------------------------------------
| Cycles | 1.0 0.0 | 1.5 | 0.0 0.0 | 0.0 0.0 | 0.0 | 1.8 | 1.7 | 0.0 |
---------------------------------------------------------------------------------------
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | |
---------------------------------------------------------------------------------
| 1 | 0.1 | 0.2 | | | | 0.3 | 0.4 | | CP | mov rax, 0xaaaaaaaaaaaaaaab
| 2 | | 1.0 | | | | | 1.0 | | CP | mul rcx
| 1 | 0.9 | | | | | | 0.1 | | CP | shr rdx, 0x1
| 1 | | | | | | 1.0 | | | CP | lea rax, ptr [rdx+rdx*2]
| 1 | | 0.3 | | | | 0.4 | 0.2 | | CP | sub rcx, rax
| 1* | | | | | | | | | | mov rax, rcx
Total Num Of Uops: 7
NEG + ADD
Intel(R) Architecture Code Analyzer Version - 2.2 build:356c3b8 (Tue, 13 Dec 2016 16:25:20 +0200)
Binary Format - 64Bit
Architecture - HSW
Analysis Type - Throughput
Throughput Analysis Report
--------------------------
Block Throughput: 2.15 Cycles Throughput Bottleneck: Dependency chains (possibly between iterations)
Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
---------------------------------------------------------------------------------------
| Cycles | 1.1 0.0 | 2.0 | 0.0 0.0 | 0.0 0.0 | 0.0 | 2.0 | 2.0 | 0.0 |
---------------------------------------------------------------------------------------
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | |
---------------------------------------------------------------------------------
| 1 | 0.1 | 0.9 | | | | 0.1 | 0.1 | | | mov rax, 0xaaaaaaaaaaaaaaab
| 2 | | 1.0 | | | | | 1.0 | | CP | mul rcx
| 1 | 1.0 | | | | | | | | CP | shr rdx, 0x1
| 1 | | | | | | 1.0 | | | CP | lea rax, ptr [rdx+rdx*2]
| 1 | | 0.1 | | | | 0.8 | 0.1 | | CP | neg rax
| 1 | 0.1 | | | | | 0.1 | 0.9 | | CP | add rcx, rax
| 1* | | | | | | | | | | mov rax, rcx
Total Num Of Uops: 8
Итак, насколько я могу судить, NEG
+ ADD
увеличивает размер кода, увеличивает количество μops, увеличивает давление для портов выполнения и увеличивает количество циклов, что приводит к чистому уменьшению по сравнению с SUB
. Итак, почему компилятор Intel делает это?
Это просто какая-то причуда генератора кода, о которой следует сообщать как о дефекте, или я пропустил некоторые достоинства в своем анализе?