Pourquoi le compilateur Intel préfère-t-il NEG + ADD sur SUB?

En examinant la sortie de divers compilateurs pour une variété d’extraits de code, j’ai remarqué que le compilateur C (ICC) d’Intel a une forte tendance à émettre deux instructions NEG + ADD où d’autres compilateurs utiliseraient une seule instruction SUB .

Comme exemple simple, considérons le code C suivant:

 uint64_t Mod3(uint64_t value) { return (value % 3); } 

ICC traduit cela au code machine suivant (quel que soit le 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) généreront tous du code essentiellement équivalent, sauf que la séquence NEG + ADD est remplacée par une seule instruction SUB .

Comme je l’ai dit, ce n’est pas seulement un casse-tête sur la façon dont ICC comstack cet extrait particulier. C’est un modèle que j’ai observé à plusieurs resockets lors de l’parsing du déassembly pour les opérations arithmétiques. Normalement, je ne pense pas beaucoup à cela, sauf que ICC est connu pour être un très bon compilateur d’optimisation et qu’il est développé par des personnes qui ont des informations privilégiées sur leurs microprocesseurs.

Pourrait-il y avoir quelque chose qu’Intel sait sur l’implémentation de l’instruction SUB sur leurs processeurs, ce qui le rend plus optimal pour le décomposer en instructions NEG + ADD ? Utiliser des instructions de type RISC qui se décodent en µops plus simples est un conseil d’optimisation bien connu pour les microarchitectures modernes, il est donc possible que SUB soit décomposé en interne en NEG et ADD µops, et qu’il soit effectivement plus efficace utiliser ces instructions “plus simples”? Les processeurs modernes sont compliqués , donc tout est possible.

Les tableaux d’instructions complets d’Agner Fog confirment mon intuition, à savoir qu’il s’agit en fait d’une pessimisation. SUB est aussi efficace que ADD sur tous les processeurs, de sorte que l’instruction NEG supplémentaire requirejse ne fait que ralentir les choses.

J’ai également exécuté les deux séquences via le propre parsingur d’ architecture de Intel pour parsingr le débit. Bien que le nombre de cycles exact et les liaisons de ports varient d’une micro-architecture à une autre, un seul SUB semble être à tous égards supérieur, de Nehalem à Broadwell. Voici les deux rapports générés par l’outil pour Haswell:

SOUS

 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 

Donc, pour autant que je NEG , NEG + ADD augmente la taille du code, augmente le nombre de µops, augmente la pression pour les ports d’exécution et augmente le nombre de cycles, entraînant ainsi une diminution nette du débit par rapport à SUB . Alors, pourquoi le compilateur d’Intel fait-il cela?

Est-ce que ce n’est qu’un détail du générateur de code qui devrait être signalé comme un défaut ou est-ce que mon parsing me manque?

Curieusement, j’ai une réponse simple: parce que le CCI n’est pas optimal.

Lorsque vous écrivez votre propre compilateur, vous commencez avec un ensemble de codes d’opération très basique: NOP , MOV , ADD … jusqu’à 10 opcodes. Vous n’utilisez pas SUB pendant un certain temps car il pourrait facilement être remplacé par: ADD NEGgative operand . NEG n’est pas basique aussi, car il pourrait être remplacé par: XOR FFFF...; ADD 1 XOR FFFF...; ADD 1 .

Vous implémentez donc plutôt un adressage binary complexe de types et de tailles d’opérandes. Vous le faites pour une seule instruction de code machine (par exemple, ADD ) et prévoyez de l’utiliser davantage pour la plupart des autres instructions. Mais à ce moment, votre collègue termine la mise en œuvre du calcul optimal du rest sans utiliser SUB ! Imaginez – il s’appelle déjà “Optimal_Mod”, vous manquez donc quelque chose d’inoptimal, non pas parce que vous êtes un méchant et que vous détestez AMD, mais juste parce que vous voyez – c’est déjà dit optimal, optimisé.

Le compilateur Intel est assez bon en général, mais il a un long historique des versions, il peut donc se comporter bizarrement dans de rares cas. Je vous suggère d’informer Intel de ce problème et de voir ce qui se passera.