Подтвердить что ты не робот

Почему компилятор Intel предпочитает NEG + ADD над SUB?

При анализе вывода различных компиляторов для множества фрагментов кода я заметил, что компилятор 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:

SUB
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 делает это?

Это просто какая-то причуда генератора кода, о которой следует сообщать как о дефекте, или я пропустил некоторые достоинства в своем анализе?

4b9b3361