En examinant les résultats de divers compilateurs pour une variété d'extraits de code, j'ai remarqué que le compilateur C d'Intel (CPI) a une forte tendance à préférer émettant une paire de NEG
+ADD
des instructions où d'autres compilateurs serait d'utiliser un seul SUB
enseignement.
Comme simple exemple, considérez les points suivants du code C:
uint64_t Mod3(uint64_t value)
{
return (value % 3);
}
La CPI se traduit par ce à la suite du code machine (indépendamment du niveau d'optimisation):
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
Alors que d'autres compilateurs (y compris MSVC, GCC et Clang) seront tous générer essentiellement code équivalent, sauf que l' NEG
+ADD
séquence est remplacée par un seul SUB
enseignement.
Comme je l'ai dit, ce n'est pas juste un caprice de la façon dont la CPI compile ce code. C'est un modèle que j'ai observé à plusieurs reprises lors de l'analyse du démontage pour les opérations arithmétiques. Normalement, je ne pense pas que beaucoup de cela, sauf que la CPI est connu pour être un très bon compilateur optimisant et il est développé par des gens qui ont initié des informations sur leurs microprocesseurs.
Pourrait-il y avoir quelque chose que Intel sait à propos de la mise en œuvre de l' SUB
instruction sur leurs processeurs qui le rend plus optimale afin de le décomposer en NEG
+ADD
instructions? À l'aide de RISC-style instructions qui décodent dans le plus simple µops est bien connu de l'optimisation des conseils pour moderne microarchitectures, ainsi est-il possible qu' SUB
se décompose en interne en NEG
et ADD
µops, et qu'il est en fait plus efficace pour le front-end décodeur à l'utilisation de ces "simple" instructions? Les Processeurs modernes sont complexes, donc, tout est possible.
Agner le Brouillard de l'instruction complète des tables de confirmer mon intuition, cependant, que c'est en fait une pessimization. SUB
est aussi efficace que ADD
sur tous les processeurs, donc additionnelles exigées NEG
instruction sert seulement ralentir les choses.
J'ai également couru les deux séquences par le biais d'Intel Architecture propre Analyseur de Code à analyser le débit. Mais le comptage de cycles et port liaisons varient d'une microarchitecture à l'autre, d'un seul SUB
semble être supérieure dans tous les domaines de Nehalem à Broadwell. Voici les deux rapports générés par l'outil pour 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+AJOUTERIntel(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
Donc, autant que je peux dire, NEG
+ADD
augmente la taille du code, augmente le nombre de µops, augmente la pression pour l'exécution des ports, et augmente le nombre de cycles, ce qui résulte en une diminution nette du débit par rapport à l' SUB
. Alors, pourquoi est-Intel compilateur de faire ceci?
Est-ce juste un caprice du générateur de code qui doivent être déclarés comme un défaut, ou alors j'ai loupé quelque mérite dans mon analyse?