La longueur du code d’assemblage peut indiquer la vitesse d’exécution?

J’apprends le C, considérons l’extrait de code suivant:

#include  int main(void) { int fahr; float calc; for (fahr = 300; fahr >= 0; fahr = fahr - 20) { calc = (5.0 / 9.0) * (fahr - 32); printf("%3d %6.1f\n", fahr, calc); } return 0; } 

Ce qui est en train d’imprimer la table de conversion de 300 à 0 en degrés Celsius en Fahrenheit. Je le comstack avec:

 $ clang -std=c11 -Wall -g -O3 -march=native main.c -o main 

Je génère aussi du code assembleur avec cette commande:

 $ clang -std=c11 -Wall -S -masm=intel -O3 -march=native main.c -o main 

Ce qui génère un fichier de 1,26 Ko et 71 lignes.

J’ai légèrement édité le code et déplacé la logique dans une autre fonction qui est initalisée à main ():

 #include  void foo(void) { int fahr; float calc; for (fahr = 300; fahr >= 0; fahr = fahr - 20) { calc = (5.0 / 9.0) * (fahr - 32); printf("%3d %6.1f\n", fahr, calc); } } int main(void) { foo(); return 0; } 

Cela générera un code assembleur de 2,33 Ko avec 128 lignes.

Exécution des deux programmes avec time ./main Je ne vois aucune différence dans la vitesse d’exécution.

Ma question est la suivante: est-il important d’essayer d’optimiser vos programmes C en fonction de la longueur du code d’assemblage?

Il semble que vous compariez la taille des fichiers .S générés par GCC, car cela n’a manifestement aucun sens. Je prétends simplement que vous avez été confronté à la taille binary de deux extraits de code générés par GCC.

Même si toutes les conditions sont identiques, une taille de code réduite peut augmenter la vitesse (en raison d’une densité de code plus élevée), en général, les CPU x86 sont suffisamment complexes pour nécessiter un découplage entre optimisations de la taille de code et optimisations de la vitesse de code.

Spécifiquement, si vous visez la vitesse du code, vous devez optimiser … la vitesse du code. Parfois, cela nécessite de choisir l’extrait le plus court, parfois pas.

Prenons l’exemple classique de l’optimisation du compilateur, multiplication par deux:

 int i = 4; i = i * 8; 

Cela peut être mal traduit par:

 ;NO optimizations at all mov eax, 4 ;i = 4 B804000000 0-1 clocks imul eax, 8 ;i = i * 8 6BC009 3 clocks ;eax = i 8 bytes total 3-4 clocks total ;Slightly optimized ;4*8 gives no sign issue, we can use shl mov eax, 4 ;i = 4 B804000000 0-1 clocks shl eax, 3 ;i = i * 8 C1E003 1 clock ;eax = i 8 bytes total 1-2 clocks total 

Les deux extraits ont la même longueur de code, mais le second est presque deux fois plus rapide.

Il s’agit d’un exemple très simple 1 , dans lequel il n’est même pas vraiment nécessaire de prendre en compte la micro-architecture.

Voici un autre exemple plus subtil, tiré de la discussion d’Agner Fog sur les blocages de registre partiel 2 :

 ;Version A Version B mov al, byte ptr [mem8] movzx ebx, byte ptr [mem8] mov ebx, eax and eax, 0ffffff00h or ebx, eax ;7 bytes 14 bytes 

Les deux versions donnent le même résultat, mais la version B est 5 à 6 horloges plus rapide que la version A, bien que la première soit deux fois plus grande que la dernière.


La réponse est alors non, la taille du code ne suffit pas; ce peut être un bris d’égalité cependant .

Si vous êtes vraiment intéressé par l’optimisation de l’assemblage, vous apprécierez ces deux lectures:

  • Les classiques d’Agner Fog .
  • Manuel d’optimisation Intel

Le premier lien contient également un manuel pour optimiser le code C et C ++.


Si vous écrivez en C, souvenez-vous que les optimisations les plus impactantes sont: 1) la manière dont les données sont représentées / stockées, c.-à-d. Les structures de données 2) le traitement des données, c.-à-d. Les algorithmes.
Il y a les optimisations de macro.

La prise en compte de l’assemblage généré passe à la micro-optimisation et les outils les plus utiles sont 1) Un compilateur intelligent 2) Un bon ensemble d’insortingnsèques 3 .


1 Tellement simple d’être optimisé en pratique.
2 Peut-être un peu obsolète maintenant, mais cela sert à cela.
3 Fonctions intégrées, non standard, traduites en instructions de assembly spécifiques.

Comme toujours, la réponse est “ça dépend”. Parfois, allonger le code le rend plus efficace: par exemple, le processeur n’a pas à gaspiller d’instructions supplémentaires en sautant après chaque boucle. Un exemple classique (littéralement “classique”: 1983!) Est “Duff’s Device” . Le code suivant

 register short *to, *from; register count; { do { /* count > 0 assumed */ *to = *from++; } while(--count > 0); } 

a été rendu beaucoup plus rapide en utilisant ce code beaucoup plus volumineux et plus compliqué:

 register short *to, *from; register count; { register n = (count + 7) / 8; switch (count % 8) { case 0: do { *to = *from++; case 7: *to = *from++; case 6: *to = *from++; case 5: *to = *from++; case 4: *to = *from++; case 3: *to = *from++; case 2: *to = *from++; case 1: *to = *from++; } while (--n > 0); } } 

Mais cela peut être poussé à l’extrême: un code trop volumineux augmente les erreurs de cache et bien d’autres problèmes. En bref: “l’optimisation prématurée est diabolique” – vous devez tester votre avant et après, et souvent sur plusieurs plates-formes, avant de décider que c’est une bonne idée.

Et je vous demanderai: la deuxième version du code ci-dessus est-elle “meilleure” que la première version? Il est moins lisible, moins facile à maintenir et beaucoup plus complexe que le code qu’il remplace.

Le code qui s’exécute est le même dans les deux cas, après l’inline. La deuxième voie est plus grande car elle doit également émettre une définition autonome de la fonction, non intégrée à la main .

Vous auriez évité cela si vous aviez utilisé static dans la fonction, afin que le compilateur sache que rien ne pouvait l’appeler de l’extérieur de l’unité de compilation. Par conséquent, une définition autonome n’était pas nécessaire si elle était alignée dans son seul appelant. .

En outre, la plupart des lignes .s dans la sortie du compilateur sont des commentaires ou des directives d’assembleur, et non des instructions. Donc, vous ne comptez même pas les instructions.

L’explorateur de compilateur Godbolt est un bon moyen de regarder la sortie asm du compilateur, avec uniquement les instructions et les étiquettes réellement utilisées. Regardez votre code ici .


Compter le nombre total d’instructions dans l’exécutable est totalement faux s’il y a des boucles ou des twigs. Ou surtout les appels de fonction à l’intérieur des boucles, comme dans ce cas. Le nombre d’instructions dynamics (nombre d’instructions réellement exécutées, c’est-à-dire comptant chaque fois à travers des boucles, etc.) est très corrélé aux performances sqrt, cache cache, et / ou mauvaise prédiction de twig).

Pour en savoir plus sur ce qui ralentit ou accélère le code, consultez le wiki des balises x86 , en particulier le contenu d’Agner Fog .

J’ai aussi récemment écrit une réponse à Deoptimizing d’un programme pour le pipeline dans les processeurs Intel Sandybridge . Penser à des moyens diaboliques de ralentir un programme est un exercice amusant.