Quand l'assemblage est-il plus rapide que C?


476

L'une des raisons invoquées pour connaître l'assembleur est que, à l'occasion, il peut être utilisé pour écrire du code qui sera plus performant que d'écrire ce code dans un langage de niveau supérieur, C en particulier. Cependant, j'ai également entendu dire à plusieurs reprises que bien que ce ne soit pas entièrement faux, les cas où l'assembleur peut en fait être utilisé pour générer du code plus performant sont à la fois extrêmement rares et nécessitent une connaissance et une expérience spécialisées de l'assemblage.

Cette question n'entre même pas dans le fait que les instructions d'assembleur seront spécifiques à la machine et non portables, ni aucun des autres aspects de l'assembleur. Il existe de nombreuses bonnes raisons de connaître l'assembly en plus de celui-ci, bien sûr, mais cela est censé être une question spécifique sollicitant des exemples et des données, pas un discours étendu sur l'assembleur par rapport aux langages de niveau supérieur.

Quelqu'un peut-il fournir des exemples spécifiques de cas où l'assemblage sera plus rapide qu'un code C bien écrit à l'aide d'un compilateur moderne, et pouvez-vous soutenir cette revendication avec des preuves de profilage? Je suis assez confiant que ces cas existent, mais je veux vraiment savoir exactement à quel point ces cas sont ésotériques, car cela semble être un sujet de controverse.


17
en fait, il est assez trivial d'améliorer le code compilé. Toute personne ayant une solide connaissance du langage d'assemblage et du C peut le voir en examinant le code généré. Tout simple est la première falaise de performances dont vous tombez lorsque vous manquez de registres jetables dans la version compilée. En moyenne, le compilateur fera bien mieux qu'un humain pour un grand projet, mais il n'est pas difficile dans un projet de taille décente de trouver des problèmes de performances dans le code compilé.
old_timer

14
En fait, la réponse courte est: l'assembleur est toujours plus rapide ou égal à la vitesse de C. La raison est que vous pouvez avoir un assemblage sans C, mais vous ne pouvez pas avoir C sans assemblage (sous la forme binaire, que nous dans l'ancien jours appelés "code machine"). Cela dit, la réponse est la suivante: les compilateurs C sont assez bons pour optimiser et "penser" à des choses auxquelles vous ne pensez généralement pas, donc cela dépend vraiment de vos compétences, mais normalement vous pouvez toujours battre le compilateur C; ce n'est encore qu'un logiciel qui ne peut pas penser et obtenir des idées. Vous pouvez également écrire un assembleur portable si vous utilisez des macros et que vous êtes patient.

11
Je ne suis pas du tout d'accord que les réponses à cette question doivent être "basées sur l'opinion" - elles peuvent être assez objectives - ce n'est pas quelque chose comme essayer de comparer les performances des langues préférées des animaux de compagnie, pour lesquelles chacune aura des points forts et des inconvénients. Il s'agit de comprendre jusqu'où les compilateurs peuvent nous emmener et à partir de quel moment il vaut mieux prendre le relais.
jsbueno

21
Plus tôt dans ma carrière, j'écrivais beaucoup d'assembleur C et mainframe dans une société de logiciels. Un de mes pairs était ce que j'appellerais un "assembleur puriste" (tout devait être assembleur), donc je parie que je pourrais écrire une routine donnée qui s'exécutait plus rapidement en C que ce qu'il pouvait écrire en assembleur. J'ai gagné. Mais pour couronner le tout, après avoir gagné, je lui ai dit que je voulais un deuxième pari - que je pouvais écrire quelque chose de plus rapide en assembleur que le programme C qui l'avait battu lors de la mise précédente. J'ai gagné cela aussi, prouvant que la plus grande partie se résume à l'habileté et à la capacité du programmeur plus que tout le reste.
Valerie R

3
À moins que votre cerveau n'ait un -O3drapeau, vous feriez probablement mieux de laisser l'optimisation au compilateur C :-)
paxdiablo

Réponses:


273

Voici un exemple réel: le point fixe se multiplie sur les anciens compilateurs.

Ceux-ci ne sont pas seulement utiles sur les appareils sans virgule flottante, ils brillent en termes de précision car ils vous donnent 32 bits de précision avec une erreur prévisible (float n'a que 23 bits et il est plus difficile de prédire la perte de précision). c'est-à-dire une précision absolue uniforme sur toute la plage, au lieu d' une précision relative proche de l'uniforme ( float).


Les compilateurs modernes optimisent bien cet exemple à virgule fixe, donc pour des exemples plus modernes qui nécessitent encore du code spécifique au compilateur, consultez


C n'a pas d'opérateur de multiplication complète (résultat de 2 N bits à partir d'entrées N bits). La façon habituelle de l'exprimer en C est de convertir les entrées en type plus large et d'espérer que le compilateur reconnaîtra que les bits supérieurs des entrées ne sont pas intéressants:

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

Le problème avec ce code est que nous faisons quelque chose qui ne peut pas être directement exprimé en langage C. Nous voulons multiplier deux nombres de 32 bits et obtenir un résultat de 64 bits dont nous retournons le 32 bits du milieu. Cependant, en C, cette multiplication n'existe pas. Tout ce que vous pouvez faire est de promouvoir les entiers en 64 bits et de faire une multiplication 64 * 64 = 64.

x86 (et ARM, MIPS et autres) peuvent cependant faire la multiplication en une seule instruction. Certains compilateurs ignoraient ce fait et généraient du code qui appelle une fonction de bibliothèque d'exécution pour effectuer la multiplication. Le décalage de 16 est également souvent effectué par une routine de bibliothèque (le x86 peut également effectuer de tels décalages).

Il nous reste donc un ou deux appels de bibliothèque juste pour une multiplication. Cela a de graves conséquences. Non seulement le décalage est plus lent, les registres doivent être préservés dans les appels de fonction et cela n'aide pas non plus à aligner et à dérouler le code.

Si vous réécrivez le même code dans l'assembleur (en ligne), vous pouvez obtenir une augmentation de vitesse significative.

En plus de cela: l'utilisation d'ASM n'est pas la meilleure façon de résoudre le problème. La plupart des compilateurs vous permettent d'utiliser certaines instructions d'assembleur sous forme intrinsèque si vous ne pouvez pas les exprimer en C. Le compilateur VS.NET2008, par exemple, expose le mul 32 * 32 = 64 bits comme __emul et le décalage 64 bits comme __ll_rshift.

En utilisant intrinsèques, vous pouvez réécrire la fonction de manière à ce que le compilateur C ait une chance de comprendre ce qui se passe. Cela permet au code d'être aligné, alloué au registre, l'élimination de la sous-expression commune et la propagation constante peuvent également être effectuées. Vous obtiendrez ainsi une énorme amélioration des performances par rapport au code assembleur manuscrit.

Pour référence: Le résultat final pour le mul à virgule fixe pour le compilateur VS.NET est:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

La différence de performance des divisions en virgule fixe est encore plus grande. J'ai eu des améliorations jusqu'au facteur 10 pour le code à point fixe lourd de division en écrivant quelques lignes asm.


L'utilisation de Visual C ++ 2013 donne le même code d'assembly dans les deux sens.

gcc4.1 de 2007 optimise également la version C pure. (L'explorateur du compilateur Godbolt n'a pas de versions antérieures de gcc installées, mais il est probable que même les anciennes versions de GCC pourraient le faire sans intrinsèques.)

Voir source + asm pour x86 (32 bits) et ARM sur l'explorateur du compilateur Godbolt . (Malheureusement, il n'a pas de compilateurs assez vieux pour produire du mauvais code à partir de la simple version C pure.)


Les processeurs modernes peuvent faire des choses C n'a pas d'opérateurs du tout , comme popcntou bit-scan pour trouver le premier ou le dernier bit défini . (POSIX a une ffs()fonction, mais sa sémantique ne correspond pas à x86 bsf/ bsr. Voir https://en.wikipedia.org/wiki/Find_first_set ).

Certains compilateurs peuvent parfois reconnaître une boucle qui compte le nombre de bits définis dans un entier et le compiler en une popcntinstruction (si activé au moment de la compilation), mais il est beaucoup plus fiable à utiliser __builtin_popcntdans GNU C, ou sur x86 si vous êtes seulement cibler le matériel avec SSE4.2: à _mm_popcnt_u32partir de<immintrin.h> .

Ou en C ++, attribuez à a std::bitset<32>et utilisez .count(). (Il s'agit d'un cas où le langage a trouvé un moyen d'exposer de manière portable une implémentation optimisée de popcount via la bibliothèque standard, d'une manière qui se compilera toujours en quelque chose de correct, et peut tirer parti de tout ce que la cible prend en charge.) Voir aussi https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .

De même, ntohlpeut être compilé vers bswap(échange d'octets x86 32 bits pour la conversion endian) sur certaines implémentations C qui en sont dotées.


Un autre domaine majeur de l'intrinsèque ou de l'asm manuscrit est la vectorisation manuelle avec des instructions SIMD. Les compilateurs ne sont pas mauvais avec des boucles simples comme dst[i] += src[i] * 10.0;, mais font souvent mal ou ne se vectorisent pas du tout quand les choses deviennent plus compliquées. Par exemple, il est peu probable que vous obteniez quelque chose comme Comment implémenter atoi en utilisant SIMD? généré automatiquement par le compilateur à partir du code scalaire.


6
Que diriez-vous des choses comme {x = c% d; y = c / d;}, les compilateurs sont-ils assez intelligents pour en faire un seul div ou idiv?
Jens Björnhager

4
En fait, un bon compilateur produirait le code optimal à partir de la première fonction. Obscurcir le code source avec des assemblages intrinsèques ou en ligne sans aucun avantage n'est pas la meilleure chose à faire.
Slacker

65
Salut Slacker, je pense que vous n'avez jamais eu à travailler sur du code à temps critique auparavant ... l'assemblage en ligne peut faire une * énorme différence. De plus, pour le compilateur, un intrinsèque est le même que l'arithmétique normale en C. C'est le point intrinsèque. Ils vous permettent d'utiliser une fonctionnalité d'architecture sans avoir à gérer les inconvénients.
Nils Pipenbrinck

6
@slacker En fait, le code ici est assez lisible: le code en ligne effectue une opération unique, qui est immédiatement compréhensible en lisant la signature de la méthode. Le code n'a perdu que lentement en lisibilité lorsqu'une instruction obscure est utilisée. Ce qui importe ici, c'est que nous avons une méthode qui ne fait qu'une seule opération clairement identifiable, et c'est vraiment la meilleure façon de produire du code lisible pour ces fonctions atomiques. À propos, ce n'est pas si obscur qu'un petit commentaire comme / * (a * b) >> 16 * / ne peut pas l'expliquer immédiatement.
Dereckson

5
Pour être juste, cet exemple est mauvais, au moins aujourd'hui. Les compilateurs C ont longtemps été en mesure de faire une multiplication 32x32 -> 64 même si le langage ne le propose pas directement: ils reconnaissent que lorsque vous transtypez des arguments 32 bits en 64 bits puis les multipliez, il n'a pas besoin de faire une multiplication complète de 64 bits, mais qu'un 32x32 -> 64 fera très bien. J'ai vérifié et tous les clang, gcc et MSVC dans leur version actuelle ont bien compris . Ce n'est pas nouveau - je me souviens avoir regardé la sortie du compilateur et l'avoir remarqué il y a dix ans.
BeeOnRope

143

Il y a plusieurs années, j'enseignais à quelqu'un de programmer en C. L'exercice consistait à faire pivoter un graphique de 90 degrés. Il est revenu avec une solution qui a pris plusieurs minutes à compléter, principalement parce qu'il utilisait des multiplications et des divisions, etc.

Je lui ai montré comment reformuler le problème en utilisant des décalages de bits, et le temps de traitement est descendu à environ 30 secondes sur le compilateur non optimisant qu'il avait.

Je venais de recevoir un compilateur d'optimisation et le même code faisait pivoter le graphique en moins de 5 secondes. J'ai regardé le code d'assembly que le compilateur générait, et d'après ce que j'ai vu, j'ai décidé que mes jours d'écriture d'assembleur étaient terminés.


3
Oui, c'était un système monochrome un bit, en particulier c'était les blocs d'images monochromes sur une Atari ST.
lilburne

16
Le compilateur d'optimisation a-t-il compilé le programme d'origine ou votre version?
Thorbjørn Ravn Andersen

Sur quel processeur? Sur 8086, je m'attendrais à ce que le code optimal pour une rotation 8x8 charge DI avec 16 bits de données en utilisant SI, répète, add di,di / adc al,al / add di,di / adc ah,ahetc. pour les huit registres 8 bits, puis répète les 8 registres, puis répète toute la procédure trois plusieurs fois, et enfin enregistrer quatre mots dans ax / bx / cx / dx. Pas question pour un assembleur de s'en approcher.
supercat

1
Je ne peux vraiment pas penser à une plate-forme où un compilateur serait susceptible d'obtenir un facteur ou deux de code optimal pour une rotation 8x8.
supercat

65

À peu près à chaque fois que le compilateur voit du code à virgule flottante, une version manuscrite sera plus rapide si vous utilisez un ancien mauvais compilateur. ( Mise à jour 2019: ce n'est pas vrai en général pour les compilateurs modernes. Surtout lors de la compilation pour autre chose que x87; les compilateurs ont plus de facilité avec SSE2 ou AVX pour les calculs scalaires, ou tout non-x86 avec un jeu de registres FP plat, contrairement à x87 enregistrer la pile.)

La raison principale est que le compilateur ne peut effectuer aucune optimisation robuste. Consultez cet article de MSDN pour une discussion sur le sujet. Voici un exemple où la version d'assembly est deux fois plus rapide que la version C (compilée avec VS2K5):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

Et quelques chiffres de mon PC exécutant une version par défaut * :

  C code: 500137 in 103884668
asm code: 500137 in 52129147

Par intérêt, j'ai échangé la boucle avec un dec / jnz et cela n'a fait aucune différence pour les timings - parfois plus rapides, parfois plus lents. Je suppose que l'aspect mémoire limité éclipse les autres optimisations. (NDLR: il est plus probable que le goulot d'étranglement de latence FP soit suffisant pour masquer le coût supplémentaire de loop. Faire deux sommations de Kahan en parallèle pour les éléments pairs / impairs, et ajouter ceux à la fin, pourrait peut-être accélérer cela par un facteur de 2. )

Oups, j'exécutais une version légèrement différente du code et il sortait les chiffres dans le mauvais sens (c'est-à-dire que C était plus rapide!). Correction et mise à jour des résultats.


20
Ou dans GCC, vous pouvez délier les mains du compilateur sur l'optimisation en virgule flottante (tant que vous promettez de ne rien faire avec des infinis ou des NaN) en utilisant l'indicateur -ffast-math. Ils ont un niveau d'optimisation, -Ofastqui est actuellement équivalent à -O3 -ffast-math, mais à l'avenir peut inclure plus d'optimisations qui peuvent conduire à une génération de code incorrecte dans les cas d'angle (comme le code qui repose sur les NaN IEEE).
David Stone

2
Oui, les flottants ne sont pas commutatifs, le compilateur doit faire EXACTEMENT ce que vous avez écrit, essentiellement ce que @DavidStone a dit.
Alec Teal

2
Avez-vous essayé les mathématiques SSE? Les performances ont été l'une des raisons pour lesquelles MS a abandonné complètement x87 en x86_64 et le double long de 80 bits en x86
phuclv

4
@Praxeolitic: FP add est commutatif ( a+b == b+a), mais pas associatif (réorganisation des opérations, donc l'arrondi des intermédiaires est différent). re: ce code: je ne pense pas qu'un x87 non commenté et une loopinstruction soient une démonstration très impressionnante de fast asm. loopn'est apparemment pas un goulot d'étranglement en raison de la latence de la PF. Je ne sais pas s'il pipeline des opérations de PF ou non; x87 est difficile à lire pour les humains. Deux fstp resultsinsns à la fin ne sont clairement pas optimaux. Il serait préférable de supprimer le résultat supplémentaire de la pile avec un non-stockage. Comme l' fstp st(0)IIRC.
Peter Cordes

2
@PeterCordes: Une conséquence intéressante de rendre l'addition commutative est que tandis que 0 + x et x + 0 sont équivalents, aucun n'est toujours équivalent à x.
supercat

58

Sans donner d'exemple spécifique ni de preuve de profileur, vous pouvez écrire un meilleur assembleur que le compilateur lorsque vous en savez plus que le compilateur.

Dans le cas général, un compilateur C moderne en sait beaucoup plus sur la façon d'optimiser le code en question: il sait comment fonctionne le pipeline du processeur, il peut essayer de réorganiser les instructions plus rapidement qu'un humain, et ainsi de suite - c'est fondamentalement la même chose que un ordinateur étant aussi bon ou meilleur que le meilleur joueur humain pour les jeux de société, etc. simplement parce qu'il peut effectuer des recherches dans l'espace problématique plus rapidement que la plupart des humains. Bien que vous puissiez théoriquement fonctionner aussi bien que l'ordinateur dans un cas spécifique, vous ne pouvez certainement pas le faire à la même vitesse, ce qui le rend irréalisable pour plus de quelques cas (c'est-à-dire que le compilateur vous surpassera très certainement si vous essayez d'écrire plusieurs routines dans l'assembleur).

D'un autre côté, il y a des cas où le compilateur n'a pas autant d'informations - je dirais principalement lorsqu'il travaille avec différentes formes de matériel externe, dont le compilateur n'a aucune connaissance. L'exemple principal étant probablement les pilotes de périphériques, où l'assembleur combiné à la connaissance intime d'un humain du matériel en question peut donner de meilleurs résultats qu'un compilateur C ne pourrait le faire.

D'autres ont mentionné des instructions spéciales, ce dont je parle dans le paragraphe ci-dessus - des instructions dont le compilateur peut avoir une connaissance limitée ou inexistante, permettant à un humain d'écrire du code plus rapidement.


Généralement, cette affirmation est vraie. Le compilateur fait de son mieux pour DWIW, mais dans certains cas, l'assembleur de codage manuel fait le travail lorsque les performances en temps réel sont indispensables.
spoulson

1
@Liedman: "il peut essayer de réorganiser les instructions plus rapidement qu'un humain". OCaml est connu pour être rapide et, de manière surprenante, son compilateur de code natif ocamloptignore la planification des instructions sur x86 et, à la place, laisse le soin au processeur car il peut être réorganisé plus efficacement au moment de l'exécution.
Jon Harrop

1
Les compilateurs modernes font beaucoup, et cela prendrait beaucoup trop de temps à la main, mais ils sont loin d'être parfaits. Recherchez dans les trackers de bogues de gcc ou llvm les bogues "d'optimisation manquée". Il y a beaucoup de. De plus, lors de l'écriture en asm, vous pouvez plus facilement profiter des conditions préalables telles que "cette entrée ne peut pas être négative" qui serait difficile à prouver pour un compilateur.
Peter Cordes

48

Dans mon travail, il y a trois raisons pour lesquelles je connais et utilise l'assemblage. Par ordre d'importance:

  1. Débogage - J'obtiens souvent du code de bibliothèque contenant des bogues ou une documentation incomplète. Je comprends ce qu'il fait en intervenant au niveau de l'assemblage. Je dois le faire environ une fois par semaine. Je l'utilise également comme outil pour déboguer des problèmes dans lesquels mes yeux ne repèrent pas l'erreur idiomatique en C / C ++ / C #. Regarder l'assemblée passe au-delà.

  2. Optimisation - le compilateur réussit assez bien dans l'optimisation, mais je joue dans un stade différent de la plupart. J'écris du code de traitement d'image qui commence généralement par un code qui ressemble à ceci:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    la partie «faire quelque chose» se produit généralement de l'ordre de plusieurs millions de fois (c'est-à-dire entre 3 et 30). En grattant les cycles dans cette phase "faire quelque chose", les gains de performances sont énormément amplifiés. Je ne commence généralement pas par là - je commence généralement par écrire le code pour fonctionner d'abord, puis je fais de mon mieux pour refactoriser le C pour qu'il soit naturellement meilleur (meilleur algorithme, moins de charge dans la boucle, etc.). J'ai généralement besoin de lire l'assemblage pour voir ce qui se passe et j'ai rarement besoin de l'écrire. Je fais cela peut-être tous les deux ou trois mois.

  3. faire quelque chose que la langue ne me laisse pas. Ceux-ci incluent - obtenir l'architecture du processeur et les fonctionnalités spécifiques du processeur, accéder aux drapeaux qui ne se trouvent pas dans le processeur (homme, je souhaite vraiment que C vous ait donné accès au drapeau de transport), etc. Je le fais peut-être une fois par an ou deux ans.


Tu ne tuiles pas tes boucles? :-)
Jon Harrop

1
@plinth: comment voulez-vous dire "cycles de raclage"?
lang2

@ lang2: cela signifie se débarrasser d'autant de temps superflu passé dans la boucle interne que possible - tout ce que le compilateur n'a pas réussi à extraire, ce qui peut inclure l'utilisation de l'algèbre pour lever une multiplication d'une boucle pour en faire un ajout à l'intérieur, etc.
socle

1
La mosaïque de boucles semble inutile si vous ne faites qu'un seul passage sur les données.
James M. Lay

@ JamesM.Lay: Si vous ne touchez chaque élément qu'une seule fois, un meilleur ordre de traversée peut vous donner une localisation spatiale. (par exemple, utilisez tous les octets d'une ligne de cache que vous avez touchée, au lieu de boucler les colonnes d'une matrice en utilisant un élément par ligne de cache.)
Peter Cordes

42

Ce n'est que lorsque vous utilisez des jeux d'instructions spéciales que le compilateur ne prend pas en charge.

Pour maximiser la puissance de calcul d'un CPU moderne avec plusieurs pipelines et branchements prédictifs, vous devez structurer le programme d'assemblage de manière à ce qu'il soit a) presque impossible pour un humain d'écrire b) encore plus impossible à maintenir.

En outre, de meilleurs algorithmes, structures de données et gestion de la mémoire vous donneront au moins un ordre de grandeur plus de performances que les micro-optimisations que vous pouvez faire en assemblage.


4
+1, même si la dernière phrase n'appartient pas vraiment à cette discussion - on pourrait supposer que l'assembleur n'entre en jeu qu'après que toutes les améliorations possibles de l'algorithme, etc. ont été réalisées.
mghie

18
@Matt: ASM écrit à la main est souvent beaucoup mieux sur certains des petits processeurs EE avec lesquels le support du compilateur est merdique.
Zan Lynx

5
"Uniquement lors de l'utilisation de jeux d'instructions à usage spécial" ?? Vous n'avez probablement jamais écrit de code asm optimisé à la main auparavant. Une connaissance modérément intime de l'architecture sur laquelle vous travaillez vous donne une bonne chance de générer un meilleur code (taille et vitesse) que votre compilateur. Évidemment, comme l'a commenté @mghie, vous commencez toujours à coder les meilleurs algos avec lesquels vous pouvez venir pour votre problème. Même pour de très bons compilateurs, vous devez vraiment écrire votre code C d'une manière qui mène le compilateur au meilleur code compilé. Sinon, le code généré sera sous-optimal.
ysap

2
@ysap - sur des ordinateurs réels (pas de minuscules puces intégrées sous-alimentées) en utilisation réelle, le code "optimal" ne sera pas plus rapide, car pour tout grand ensemble de données, les performances seront limitées par l'accès à la mémoire et les défauts de page ( et si vous ne disposez pas d'un grand ensemble de données, cela va être rapide dans tous les cas et il n'y a aucun intérêt à l'optimiser) - ces jours-là, je travaille principalement en C # (pas même en c) et les gains de performances du gestionnaire de mémoire de compactage sortent- pondérer les frais généraux de la collecte des ordures, du compactage et de la compilation JIT.
Nir

4
+1 pour avoir déclaré que les compilateurs (en particulier JIT) peuvent faire un meilleur travail que les humains, s'ils sont optimisés pour le matériel sur lequel ils sont exécutés.
Sebastian

38

Bien que C soit "proche" de la manipulation de bas niveau des données 8 bits, 16 bits, 32 bits et 64 bits, il existe quelques opérations mathématiques non prises en charge par C qui peuvent souvent être exécutées avec élégance dans certaines instructions d'assemblage. ensembles:

  1. Multiplication à virgule fixe: le produit de deux nombres à 16 bits est un nombre à 32 bits. Mais les règles de C indiquent que le produit de deux nombres 16 bits est un nombre 16 bits, et le produit de deux nombres 32 bits est un nombre 32 bits - la moitié inférieure dans les deux cas. Si vous voulez la moitié supérieure d'une multiplication 16x16 ou 32x32, vous devez jouer à des jeux avec le compilateur. La méthode générale consiste à transtyper sur une largeur de bit plus grande que nécessaire, à multiplier, à rétrograder et à rétrograder:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    Dans ce cas, le compilateur peut être suffisamment intelligent pour savoir que vous essayez vraiment d'obtenir la moitié supérieure d'une multiplication 16x16 et de faire la bonne chose avec la multiplication 16x16 native de la machine. Ou cela peut être stupide et nécessiter un appel de bibliothèque pour effectuer la multiplication 32x32, ce qui est excessif car vous n'avez besoin que de 16 bits du produit - mais la norme C ne vous donne aucun moyen de vous exprimer.

  2. Certaines opérations de décalage de bits (rotation / portage):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    Ce n'est pas trop inélégant en C, mais encore une fois, à moins que le compilateur ne soit assez intelligent pour réaliser ce que vous faites, il va faire beaucoup de travail "inutile". De nombreux jeux d'instructions d'assemblage vous permettent de faire pivoter ou de décaler vers la gauche / droite avec le résultat dans le registre de report, vous pouvez donc accomplir les étapes ci-dessus dans 34 instructions: charger un pointeur au début du tableau, effacer le report et effectuer 32 8- bits à droite, en utilisant l'incrémentation automatique sur le pointeur.

    Pour un autre exemple, il existe des registres à décalage à rétroaction linéaire (LFSR) qui sont élégamment exécutés en assemblage: prenez un morceau de N bits (8, 16, 32, 64, 128, etc.), déplacez le tout à droite de 1 (voir ci-dessus). algorithme), alors si le portage résultant est 1 alors vous XOR dans un motif binaire qui représente le polynôme.

Cela dit, je n'aurais pas recours à ces techniques à moins d'avoir de sérieuses contraintes de performance. Comme d'autres l'ont dit, l'assemblage est beaucoup plus difficile à documenter / déboguer / tester / maintenir que le code C: le gain de performances entraîne de sérieux coûts.

edit: 3. La détection de débordement est possible dans l'assemblage (ne peut pas vraiment le faire en C), cela rend certains algorithmes beaucoup plus faciles.


23

Réponse courte? Quelquefois.

Techniquement, chaque abstraction a un coût et un langage de programmation est une abstraction pour le fonctionnement du CPU. C est cependant très proche. Il y a des années, je me souviens avoir ri à haute voix lorsque je me suis connecté à mon compte UNIX et que j'ai reçu le message de fortune suivant (lorsque de telles choses étaient populaires):

Le langage de programmation C - Un langage qui combine la flexibilité du langage d'assemblage avec la puissance du langage d'assemblage.

C'est drôle parce que c'est vrai: C est comme un langage d'assemblage portable.

Il convient de noter que le langage d'assemblage s'exécute comme vous l'écrivez. Il existe cependant un compilateur entre C et le langage d'assemblage qu'il génère et cela est extrêmement important car la vitesse à laquelle votre code C est a beaucoup à voir avec la qualité de votre compilateur.

Lorsque gcc est entré en scène, l'une des choses qui l'ont rendu si populaire, c'est qu'il était souvent bien meilleur que les compilateurs C fournis avec de nombreuses saveurs UNIX commerciales. Non seulement c'était ANSI C (aucun de ces déchets K&R C), il était plus robuste et produisait généralement un meilleur code (plus rapide). Pas toujours mais souvent.

Je vous dis tout cela parce qu'il n'y a pas de règle générale sur la vitesse de C et de l'assembleur car il n'y a pas de norme objective pour C.

De même, l'assembleur varie beaucoup selon le processeur que vous utilisez, les spécifications de votre système, le jeu d'instructions que vous utilisez, etc. Historiquement, il y avait deux familles d'architecture CPU: CISC et RISC. Le plus grand acteur du CISC était et est toujours l'architecture Intel x86 (et le jeu d'instructions). RISC a dominé le monde UNIX (MIPS6000, Alpha, Sparc et ainsi de suite). L'ICCA a remporté la bataille des cœurs et des esprits.

Quoi qu'il en soit, la sagesse populaire quand j'étais un jeune développeur était que le x86 manuscrit pouvait souvent être beaucoup plus rapide que C parce que la façon dont l'architecture fonctionnait, il avait une complexité qui bénéficiait d'un humain. RISC, d'autre part, semblait conçu pour les compilateurs, donc personne (je le savais) n'a écrit, dit l'assembleur Sparc. Je suis sûr que de telles personnes existaient, mais sans aucun doute elles sont toutes les deux devenues folles et ont été institutionnalisées maintenant.

Les ensembles d'instructions sont un point important même dans la même famille de processeurs. Certains processeurs Intel ont des extensions comme SSE à SSE4. AMD avait ses propres instructions SIMD. L'avantage d'un langage de programmation comme C était que quelqu'un pouvait écrire sa bibliothèque, donc elle était optimisée pour le processeur sur lequel vous exécutiez. Ce fut un travail difficile en assembleur.

Il y a encore des optimisations que vous pouvez faire dans un assembleur qu'aucun compilateur ne pourrait faire et un assembleur bien écrit algoirthm sera aussi rapide ou plus rapide que son équivalent C. La plus grande question est: cela en vaut-il la peine?

En fin de compte, l'assembleur était un produit de son temps et était plus populaire à une époque où les cycles CPU étaient chers. De nos jours, un processeur dont la fabrication coûte 5 à 10 $ (Intel Atom) peut faire à peu près tout ce que l'on peut souhaiter. La seule vraie raison d'écrire l'assembleur de nos jours est pour des choses de bas niveau comme certaines parties d'un système d'exploitation (même si la grande majorité du noyau Linux est écrite en C), les pilotes de périphériques, éventuellement les périphériques intégrés (bien que C tend à y dominer) aussi) et ainsi de suite. Ou tout simplement pour les coups de pied (ce qui est quelque peu masochiste).


Il y avait beaucoup de gens qui utilisaient l'assembleur ARM comme langue de choix sur les machines Acorn (début des années 90). IIRC, ils ont dit que le petit jeu d'instructions de risc le rendait plus facile et plus amusant. Mais je suppose que c'est parce que le compilateur C était une arrivée tardive pour Acorn, et le compilateur C ++ n'a jamais été terminé.
Andrew M

3
"... parce qu'il n'y a pas de norme subjective pour C." Vous voulez dire objectif .
Thomas le

@AndrewM: Oui, j'ai écrit des applications en plusieurs langues dans BASIC et ARM assembler pendant environ 10 ans. J'ai appris le C pendant ce temps mais ce n'était pas très utile car il est aussi lourd que l'assembleur et plus lent. Norcroft a fait des optimisations impressionnantes mais je pense que le jeu d'instructions conditionnelles était un problème pour les compilateurs de l'époque.
Jon Harrop

1
@AndrewM: eh bien, en fait, ARM est une sorte de RISC fait à l'envers. D'autres ISC RISC ont été conçues à partir de ce qu'un compilateur utiliserait. L'ARM ISA semble avoir été conçu à partir de ce que fournit le CPU (barillet shifter, drapeaux de condition → exposons-les dans chaque instruction).
ninjalj

16

Un cas d'utilisation qui pourrait ne plus s'appliquer mais pour votre plus grand plaisir: sur l'Amiga, le processeur et les puces graphiques / audio se battraient pour accéder à une certaine zone de RAM (les 2 premiers Mo de RAM pour être spécifiques). Ainsi, lorsque vous n'aviez que 2 Mo de RAM (ou moins), afficher des graphiques complexes et jouer du son réduirait les performances du processeur.

Dans l'assembleur, vous pouvez entrelacer votre code d'une manière si intelligente que le CPU n'essaiera d'accéder à la RAM que lorsque les puces graphiques / audio sont occupées en interne (c'est-à-dire lorsque le bus est libre). Donc, en réorganisant vos instructions, en utilisant intelligemment le cache du processeur, la synchronisation du bus, vous pouvez obtenir des effets qui n'étaient tout simplement pas possibles en utilisant un langage de niveau supérieur car vous deviez chronométrer chaque commande, même insérer des NOP ici et là pour garder les divers puces hors de l'autre radar.

C'est une autre raison pour laquelle l'instruction NOP (Aucune opération - ne rien faire) du CPU peut réellement faire fonctionner votre application plus rapidement.

[EDIT] Bien sûr, la technique dépend d'une configuration matérielle spécifique. Ce qui était la principale raison pour laquelle de nombreux jeux Amiga ne pouvaient pas faire face à des processeurs plus rapides: le timing des instructions était décalé.


L'Amiga ne disposait pas de 16 Mo de RAM de puce, plus de 512 Ko à 2 Mo selon le chipset. En outre, de nombreux jeux Amiga ne fonctionnaient pas avec des processeurs plus rapides en raison de techniques telles que vous les décrivez.
bk1e

1
@ bk1e - Amiga a produit une large gamme de différents modèles d'ordinateurs, l'Amiga 500 livré avec 512 Ko de RAM étendu à 1 Mo dans mon cas. amigahistory.co.uk/amiedevsys.html est un amiga avec 128Meg Ram
David Waters

@ bk1e: Je me tiens corrigé. Ma mémoire peut me manquer mais la RAM de la puce n'était-elle pas limitée au premier espace d'adressage 24 bits (c'est-à-dire 16 Mo)? Et Fast a été cartographié au-dessus de cela?
Aaron Digulla

@Aaron Digulla: Wikipedia a plus d'informations sur les distinctions entre puce / RAM rapide / lente: en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e

@ bk1e: Mon erreur. Le processeur 68k n'avait que 24 voies d'adresse, c'est pourquoi j'avais 16 Mo en tête.
Aaron Digulla

15

Point un qui n'est pas la réponse.
Même si vous ne le programmez jamais, je trouve utile de connaître au moins un jeu d'instructions d'assembleur. Cela fait partie de la quête sans fin des programmeurs pour en savoir plus et donc être meilleur. Également utile lorsque vous entrez dans des frameworks, vous n'avez pas le code source et vous avez au moins une idée approximative de ce qui se passe. Il vous aide également à comprendre JavaByteCode et .Net IL car ils sont tous deux similaires à l'assembleur.

Pour répondre à la question lorsque vous avez une petite quantité de code ou une grande quantité de temps. Le plus utile pour une utilisation dans des puces intégrées, où une faible complexité des puces et une faible concurrence dans les compilateurs ciblant ces puces peuvent faire pencher la balance en faveur des humains. De plus, pour les appareils restreints, vous échangez souvent la taille du code / la taille de la mémoire / les performances d'une manière qu'il serait difficile de demander à un compilateur de faire. Par exemple, je sais que cette action utilisateur n'est pas appelée souvent, donc j'aurai une petite taille de code et de mauvaises performances, mais cette autre fonction qui semble similaire est utilisée chaque seconde, donc j'aurai une taille de code plus grande et des performances plus rapides. C'est le genre de compromis qu'un programmeur d'assemblage qualifié peut utiliser.

Je voudrais également ajouter qu'il y a beaucoup de terrain d'entente où vous pouvez coder en C compiler et examiner l'assembly produit, puis changer votre code C ou modifier et maintenir en tant qu'assembly.

Mon ami travaille sur des microcontrôleurs, actuellement des puces pour contrôler de petits moteurs électriques. Il travaille dans une combinaison de bas niveau c et d'assemblage. Il m'a dit une fois une bonne journée de travail où il a réduit la boucle principale de 48 instructions à 43. Il est également confronté à des choix tels que le code a grandi pour remplir la puce de 256k et que l'entreprise veut une nouvelle fonctionnalité, pensez-vous

  1. Supprimer une fonction existante
  2. Réduisez la taille de certaines ou de toutes les fonctionnalités existantes, peut-être au détriment des performances.
  3. Préconisez le passage à une puce plus grande avec un coût plus élevé, une consommation d'énergie plus élevée et un facteur de forme plus grand.

Je voudrais ajouter en tant que développeur commercial avec un portefeuille ou des langages, des plates-formes, des types d'applications que je n'ai jamais ressenti le besoin de plonger dans l'écriture d'assemblage. J'ai toujours apprécié les connaissances que j'ai acquises à ce sujet. Et parfois débogué dedans.

Je sais que j'ai beaucoup plus répondu à la question "pourquoi devrais-je apprendre l'assembleur" mais je pense que c'est une question plus importante que quand est-ce plus rapide.

alors essayons encore une fois Vous devriez penser à l'assemblage

  • travailler sur la fonction du système d'exploitation de bas niveau
  • Travailler sur un compilateur.
  • Travailler sur une puce extrêmement limitée, un système embarqué, etc.

N'oubliez pas de comparer votre assemblage au compilateur généré pour voir lequel est plus rapide / plus petit / meilleur.

David.


4
+1 pour considérer les applications embarquées sur de minuscules puces. Trop d'ingénieurs logiciels ici ne considèrent pas l'intégration ou pensent que cela signifie un téléphone intelligent (32 bits, RAM RAM, flash MB).
Martin

1
Les applications intégrées dans le temps en sont un excellent exemple! Il y a souvent des instructions étranges (même très simples comme avr sbiet cbi) que les compilateurs (et parfois encore) ne profitent pas pleinement, en raison de leur connaissance limitée du matériel.
felixphew

15

Je suis surpris que personne n'ait dit cela. La strlen()fonction est beaucoup plus rapide si elle est écrite en assembleur! En C, la meilleure chose que vous puissiez faire est

int c;
for(c = 0; str[c] != '\0'; c++) {}

pendant l'assemblage, vous pouvez l'accélérer considérablement:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

la longueur est en ecx. Cela compare 4 caractères à la fois, donc c'est 4 fois plus rapide. Et pensez qu'en utilisant le mot de poids fort eax et ebx, cela deviendra 8 fois plus rapide que la routine C précédente!


3
Comment cela se compare-t-il avec ceux de strchr.nfshost.com/optimized_strlen_function ?
ninjalj

@ninjalj: c'est la même chose :) je ne pensais pas que cela puisse être fait de cette façon en C. Cela peut être légèrement amélioré je pense
BlackBear

Il y a toujours une opération ET au niveau du bit avant chaque comparaison dans le code C. Il est possible que le compilateur soit assez intelligent pour réduire cela à des comparaisons d'octets hauts et bas, mais je ne parierais pas d'argent dessus. Il y a en fait un algorithme de boucle plus rapide qui est basé sur la propriété qui (word & 0xFEFEFEFF) & (~word + 0x80808080)est zéro si tous les octets dans word ne sont pas nuls.
user2310967

@MichaWiedenmann true, je devrais charger bx après avoir comparé les deux caractères dans ax. Merci
BlackBear

14

Les opérations matricielles utilisant des instructions SIMD sont probablement plus rapides que le code généré par le compilateur.


Certains compilateurs (le VectorC, si je me souviens bien) génèrent du code SIMD, donc même cela n'est probablement plus un argument pour utiliser le code assembleur.
OregonGhost

Les compilateurs créent du code prenant en charge SSE, de sorte que cet argument n'est pas vrai
vartec

5
Pour bon nombre de ces situations, vous pouvez utiliser les intrisiques SSE au lieu de l'assemblage. Cela rendra votre code plus portable (gcc visual c ++, 64bit, 32bit etc.) et vous n'aurez pas à faire d'allocation de registre.
Laserallan

1
Bien sûr que vous le feriez, mais la question ne demandait pas où devrais-je utiliser l'assembly au lieu de C. Il a dit quand le compilateur C ne génère pas un meilleur code. J'ai supposé une source C qui n'utilise pas d'appels SSE directs ni d'assemblage en ligne.
Mehrdad Afshari

9
Mais Mehrdad a raison. Obtenir le bon SSE est assez difficile pour le compilateur et même dans des situations évidentes (pour les humains, c'est-à-dire), la plupart des compilateurs ne l'utilisent pas.
Konrad Rudolph

13

Je ne peux pas donner d'exemples spécifiques car c'était il y a trop d'années, mais il y avait de nombreux cas où l'assembleur écrit à la main pouvait surpasser n'importe quel compilateur. Raisons pour lesquelles:

  • Vous pouvez dévier des conventions d'appel, en passant des arguments dans les registres.

  • Vous pouvez soigneusement réfléchir à l'utilisation des registres et éviter de stocker des variables en mémoire.

  • Pour des choses comme les tables de saut, vous pourriez éviter d'avoir à vérifier les limites de l'index.

Fondamentalement, les compilateurs font un très bon travail d'optimisation, et c'est presque toujours "assez bien", mais dans certaines situations (comme le rendu graphique) où vous payez cher pour chaque cycle, vous pouvez prendre des raccourcis parce que vous connaissez le code , où un compilateur ne pouvait pas parce qu'il devait être du bon côté.

En fait, j'ai entendu parler d'un code de rendu graphique où une routine, comme une routine de tracé de ligne ou de remplissage de polygone, générait en fait un petit bloc de code machine sur la pile et l'exécutait là, afin d'éviter une prise de décision continue sur le style de ligne, la largeur, le motif, etc.

Cela dit, ce que je veux qu'un compilateur fasse, c'est de générer un bon code d'assemblage pour moi mais pas trop intelligent, et ils le font surtout. En fait, une des choses que je déteste à propos de Fortran est de brouiller le code dans une tentative de «l'optimiser», généralement sans but significatif.

Habituellement, lorsque les applications ont des problèmes de performances, cela est dû à une conception inutile. Ces jours-ci, je ne recommanderais jamais l'assembleur pour les performances à moins que l'application globale n'ait déjà été réglée dans un pouce de sa vie, ne soit toujours pas assez rapide et passe tout son temps dans des boucles internes étroites.

Ajouté: J'ai vu de nombreuses applications écrites en langage assembleur, et le principal avantage de vitesse par rapport à un langage comme C, Pascal, Fortran, etc. était parce que le programmeur était beaucoup plus prudent lors du codage en assembleur. Il ou elle va écrire environ 100 lignes de code par jour, quelle que soit la langue, et dans un langage de compilation qui équivaut à 3 ou 400 instructions.


8
+1: "Vous pourriez dévier des conventions d'appel". Les compilateurs C / C ++ ont tendance à aspirer à renvoyer plusieurs valeurs. Ils utilisent souvent la forme sret où la pile des appelants alloue un bloc contigu à une structure et lui transmet une référence pour que l'appelé le remplisse. Renvoyer plusieurs valeurs dans les registres est plusieurs fois plus rapide.
Jon Harrop

1
@ Jon: Les compilateurs C / C ++ le font très bien lorsque la fonction est en ligne (les fonctions non en ligne doivent être conformes à l'ABI, ce n'est pas une limitation de C et C ++ mais le modèle de liaison)
Ben Voigt

@BenVoigt: Voici un contre-exemple flyingfrogblog.blogspot.co.uk/2012/04/…
Jon Harrop

2
Je ne vois aucun appel de fonction y être intégré.
Ben Voigt

13

Quelques exemples de mon expérience:

  • Accès à des instructions qui ne sont pas accessibles à partir de C.Par exemple, de nombreuses architectures (comme x86-64, IA-64, DEC Alpha et MIPS ou PowerPC 64 bits) prennent en charge une multiplication 64 bits par 64 bits produisant un résultat de 128 bits. GCC a récemment ajouté une extension donnant accès à ces instructions, mais avant que cet assemblage ne soit requis. Et l'accès à cette instruction peut faire une énorme différence sur les processeurs 64 bits lors de l'implémentation de quelque chose comme RSA - parfois autant qu'un facteur d'amélioration des performances.

  • Accès aux drapeaux spécifiques au CPU. Celui qui m'a beaucoup mordu est le drapeau de portage; lorsque vous effectuez un ajout à précision multiple, si vous n'avez pas accès au bit de report du processeur, vous devez plutôt comparer le résultat pour voir s'il a débordé, ce qui nécessite 3 à 5 instructions supplémentaires par membre; et pire encore, qui sont assez sériels en termes d'accès aux données, ce qui tue les performances sur les processeurs superscalaires modernes. Lors du traitement de milliers de tels entiers d'affilée, pouvoir utiliser addc est une énorme victoire (il y a aussi des problèmes superscalaires avec des conflits sur le bit de transport, mais les processeurs modernes s'en sortent plutôt bien).

  • SIMD. Même les compilateurs d'autovectorisation ne peuvent faire que des cas relativement simples, donc si vous voulez de bonnes performances SIMD, il est malheureusement souvent nécessaire d'écrire directement le code. Bien sûr, vous pouvez utiliser l'intrinsèque au lieu de l'assembly mais une fois que vous êtes au niveau intrinsèque, vous écrivez essentiellement l'assembly de toute façon, en utilisant simplement le compilateur comme un allocateur de registre et (nominalement) un planificateur d'instructions. (J'ai tendance à utiliser les intrinsèques pour SIMD simplement parce que le compilateur peut générer les prologues de la fonction et ainsi de suite pour moi, donc je peux utiliser le même code sous Linux, OS X et Windows sans avoir à traiter les problèmes ABI comme les conventions d'appel de fonction, mais d'autres que les intrinsèques SSE ne sont vraiment pas très belles - celles d'Altivec semblent meilleures même si je n'ai pas beaucoup d'expérience avec elles).Correction d'erreurs AES ou SIMD en bitslicing - on pourrait imaginer un compilateur capable d'analyser des algorithmes et de générer un tel code, mais il me semble qu'un tel compilateur intelligent est au moins à 30 ans de l'existant (au mieux).

D'un autre côté, les machines multicœurs et les systèmes distribués ont changé la plupart des gains de performances les plus importants dans l'autre sens - obtenez une accélération supplémentaire de 20% en écrivant vos boucles internes en assemblage, ou 300% en les exécutant sur plusieurs cœurs, ou 10000% par les exécuter sur un cluster de machines. Et bien sûr, les optimisations de haut niveau (des choses comme les futurs, la mémorisation, etc.) sont souvent beaucoup plus faciles à faire dans un langage de niveau supérieur comme ML ou Scala que C ou asm, et peuvent souvent fournir une performance beaucoup plus importante. Donc, comme toujours, il y a des compromis à faire.


2
@Dennis, c'est pourquoi j'ai écrit `` Bien sûr, vous pouvez utiliser les intrinsèques au lieu de l'assemblage, mais une fois que vous êtes au niveau intrinsèque, vous écrivez essentiellement l'assemblage de toute façon, en utilisant simplement le compilateur comme allocateur de registre et (nominalement) ordonnanceur d'instructions. ''
Jack Lloyd

En outre, le code SIMD intrinsèque a tendance à être moins lisible que le même code écrit dans l'assembleur: une grande partie du code SIMD repose sur des réinterprétations implicites des données dans les vecteurs, ce qui est un PITA à voir avec les types intrinsèques du compilateur de types de données.
cmaster - réintègre monica

10

Boucles serrées, comme lors de la lecture d'images, car une image peut contenir des millions de pixels. S'asseoir et déterminer comment utiliser au mieux le nombre limité de registres de processeur peut faire une différence. Voici un exemple réel:

http://danbystrom.se/2008/12/22/optimizing-away-ii/

Ensuite, les processeurs ont souvent des instructions ésotériques trop spécialisées pour que le compilateur s'en soucie, mais parfois un programmeur assembleur peut en faire bon usage. Prenez par exemple l'instruction XLAT. Vraiment génial si vous devez faire des recherches de table en boucle et que la table est limitée à 256 octets!

Mise à jour: Oh, venez à penser à ce qui est le plus crucial lorsque nous parlons de boucles en général: le compilateur n'a souvent aucune idée du nombre d'itérations qui seront le cas commun! Seul le programmeur sait qu'une boucle sera répétée BEAUCOUP de fois et qu'il sera donc avantageux de la préparer avec un peu de travail supplémentaire, ou si elle sera répétée si peu de fois que la configuration prendra réellement plus de temps que les itérations attendu.


3
L'optimisation dirigée par le profil donne au compilateur des informations sur la fréquence d'utilisation d'une boucle.
Zan Lynx

10

Plus souvent que vous ne le pensez, C doit faire des choses qui semblent inutiles du point de vue d'un codeur d'assemblage simplement parce que les normes C le disent.

La promotion entière, par exemple. Si vous voulez décaler une variable char en C, on s'attendrait généralement à ce que le code fasse en fait juste cela, un décalage d'un seul bit.

Cependant, les normes imposent au compilateur d'étendre un signe à int avant le décalage et de tronquer le résultat en char par la suite, ce qui pourrait compliquer le code en fonction de l'architecture du processeur cible.


Les compilateurs de qualité pour les petits micros ont pu pendant des années éviter de traiter les parties supérieures des valeurs dans les cas où cela ne pourrait jamais affecter de manière significative les résultats. Les règles de promotion posent des problèmes, mais le plus souvent dans les cas où un compilateur n'a aucun moyen de savoir quels cas d'angle sont et ne sont pas pertinents.
supercat

9

Vous ne savez pas vraiment si votre code C bien écrit est vraiment rapide si vous n'avez pas examiné le désassemblage de ce que le compilateur produit. Plusieurs fois, vous le regardez et voyez que "bien écrit" était subjectif.

Il n'est donc pas nécessaire d'écrire dans l'assembleur pour obtenir le code le plus rapide de tous les temps, mais cela vaut certainement la peine de connaître l'assembleur pour la même raison.


2
"Il n'est donc pas nécessaire d'écrire dans l'assembleur pour obtenir le code le plus rapide de tous les temps" Eh bien, je n'ai jamais vu un compilateur faire la chose optimale dans tous les cas qui n'était pas trivial. Un humain expérimenté peut faire mieux que le compilateur dans pratiquement tous les cas. Il est donc absolument nécessaire d'écrire dans l'assembleur pour obtenir "le code le plus rapide de tous les temps".
cmaster - réintègre monica

@cmaster D'après mon expérience, la sortie du compilateur est bien aléatoire. Parfois, c'est vraiment bon et optimal et parfois c'est "comment ces déchets ont-ils pu être émis".
sharptooth

9

J'ai lu toutes les réponses (plus de 30) et je n'ai pas trouvé de raison simple: l'assembleur est plus rapide que C si vous avez lu et pratiqué le Manuel de référence de l'optimisation des architectures Intel® 64 et IA-32 , donc la raison pour laquelle l'assemblage peut être plus lent, c'est que les personnes qui écrivent un assemblage plus lent n'ont pas lu le manuel d'optimisation .

Dans le bon vieux temps d'Intel 80286, chaque instruction était exécutée avec un nombre fixe de cycles CPU, mais depuis Pentium Pro, sorti en 1995, les processeurs Intel sont devenus superscalaires, en utilisant Complex Pipelining: exécution hors commande et renommage de registre. Avant cela, sur Pentium, produit en 1993, il y avait des pipelines U et V: des pipelines doubles qui pouvaient exécuter deux instructions simples à un cycle d'horloge si elles ne dépendaient pas l'une de l'autre; mais ce n'était rien à comparer de ce qui est Exécution Hors Ordre & Renommage de Registre apparu dans Pentium Pro, et presque inchangé de nos jours.

Pour expliquer en quelques mots, le code le plus rapide est celui où les instructions ne dépendent pas des résultats précédents, par exemple, vous devez toujours effacer les registres entiers (par movzx) ou utiliser add rax, 1 place ou inc raxpour supprimer la dépendance sur l'état précédent des indicateurs, etc.

Vous pouvez en savoir plus sur l'exécution hors-commande et le changement de nom du registre si le temps le permet, de nombreuses informations sont disponibles sur Internet.

Il y a aussi d'autres problèmes importants comme la prédiction de branche, le nombre d'unités de chargement et de stockage, le nombre de portes qui exécutent des micro-opérations, etc., mais la chose la plus importante à considérer est à savoir l'exécution hors service.

La plupart des gens ne sont tout simplement pas au courant de l'exécution hors service, ils écrivent donc leurs programmes d'assemblage comme pour 80286, s'attendant à ce que leur instruction prenne un temps fixe pour s'exécuter, quel que soit le contexte; tandis que les compilateurs C sont conscients de l'exécution hors service et génèrent correctement le code. C'est pourquoi le code de ces personnes inconscientes est plus lent, mais si vous en prenez conscience, votre code sera plus rapide.


8

Je pense que le cas général où l'assembleur est plus rapide est quand un programmeur d'assemblage intelligent regarde la sortie du compilateur et dit "c'est un chemin critique pour les performances et je peux l'écrire pour être plus efficace", puis cette personne ajuste cet assembleur ou le réécrit de zéro.


7

Tout dépend de votre charge de travail.

Pour les opérations quotidiennes, C et C ++ sont très bien, mais il y a certaines charges de travail (toutes les transformations impliquant la vidéo (compression, décompression, effets d'image, etc.)) qui nécessitent à peu près l'assemblage pour être performants.

Ils impliquent également généralement l'utilisation d'extensions de chipset spécifiques au processeur (MME / MMX / SSE / peu importe) qui sont réglées pour ce type d'opération.


6

J'ai une opération de transposition de bits qui doit être faite, sur 192 ou 256 bits à chaque interruption, qui se produit toutes les 50 microsecondes.

Cela se fait par une carte fixe (contraintes matérielles). En utilisant C, il a fallu environ 10 microsecondes pour faire. Lorsque j'ai traduit cela en assembleur, en tenant compte des caractéristiques spécifiques de cette carte, de la mise en cache de registre spécifique et de l'utilisation d'opérations orientées bits; il a fallu moins de 3,5 microsecondes pour effectuer.


6

Il pourrait être utile de regarder Optimizing Immutable and Purity de Walter Bright, ce n'est pas un test profilé, mais vous montre un bon exemple de différence entre l'ASM manuscrit et généré par le compilateur. Walter Bright écrit des compilateurs d'optimisation, donc il pourrait être utile de consulter ses autres articles de blog.



5

La réponse simple ... Celui qui connaît bien l' assemblage (aka a la référence à côté de lui, et profite de chaque petite fonctionnalité de cache de processeur et de pipeline, etc.) est garanti d'être capable de produire du code beaucoup plus rapide que tout autre compilateur.

Cependant, la différence de nos jours n'a pas d'importance dans l'application typique.


1
Vous avez oublié de dire "compte tenu de beaucoup de temps et d'efforts" et de "créer un cauchemar de maintenance". Un de mes collègues travaillait à l'optimisation d'une section critique du code du système d'exploitation, et il travaillait beaucoup plus en C que dans l'assemblage, car cela lui permettait d'étudier l'impact sur les performances des changements de haut niveau dans un délai raisonnable.
Artelius

Je suis d'accord. Parfois, vous utilisez des macros et des scripts pour générer du code d'assemblage afin de gagner du temps et de développer rapidement. La plupart des assembleurs de nos jours ont des macros; sinon, vous pouvez créer un pré-processeur de macro (simple) en utilisant un script Perl (assez simple RegEx).

Cette. Précisément. Le compilateur pour battre les experts du domaine n'a pas encore été inventé.
cmaster - réintègre monica

4

L'une des possibilités de la version CP / M-86 de PolyPascal (frère de Turbo Pascal) était de remplacer l'installation "use-bios-to-output-characters-to-the-screen" par une routine de langage machine qui, en substance, a reçu le x, et y, et la chaîne à mettre là.

Cela a permis de mettre à jour l'écran beaucoup, beaucoup plus vite qu'avant!

Il y avait de la place dans le binaire pour incorporer du code machine (quelques centaines d'octets) et il y avait aussi d'autres trucs, il était donc essentiel de presser autant que possible.

Il s'avère que puisque l'écran était de 80x25, les deux coordonnées pouvaient tenir dans un octet chacune, donc les deux pouvaient tenir dans un mot de deux octets. Cela a permis de faire les calculs nécessaires en moins d'octets puisqu'un seul ajout pouvait manipuler les deux valeurs simultanément.

À ma connaissance, il n'y a pas de compilateurs C qui peuvent fusionner plusieurs valeurs dans un registre, faire des instructions SIMD dessus et les diviser plus tard (et je ne pense pas que les instructions machine seront de toute façon plus courtes).


4

L'un des extraits d'assemblage les plus célèbres provient de la boucle de mappage de texture de Michael Abrash ( expliquée en détail ici ):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

De nos jours, la plupart des compilateurs expriment des instructions spécifiques au processeur avancées comme intrinsèques, c'est-à-dire des fonctions qui sont compilées jusqu'à l'instruction réelle. MS Visual C ++ prend en charge les éléments intrinsèques pour MMX, SSE, SSE2, SSE3 et SSE4, vous devez donc vous soucier moins de passer à l'assemblage pour tirer parti des instructions spécifiques à la plate-forme. Visual C ++ peut également tirer parti de l'architecture réelle que vous ciblez avec le paramètre / ARCH approprié.


Encore mieux, ces intrinsèques SSE sont spécifiés par Intel, ils sont donc assez portables.
James

4

Avec le bon programmeur, les programmes Assembler peuvent toujours être réalisés plus rapidement que leurs homologues C (au moins marginalement). Il serait difficile de créer un programme C où vous ne pourriez pas retirer au moins une instruction de l'assembleur.


Ce serait un peu plus correct: "Il serait difficile de créer un programme C non trivial où ..." Alternativement, vous pourriez dire: "Il serait difficile de trouver un programme C réel où ..." Le point est , il existe des boucles triviales pour lesquelles les compilateurs produisent une sortie optimale. Néanmoins, bonne réponse.
cmaster - réintègre monica


4

gcc est devenu un compilateur largement utilisé. Ses optimisations en général ne sont pas si bonnes. Beaucoup mieux que l'assembleur d'écriture programmeur moyen, mais pour de vraies performances, pas si bon. Il existe des compilateurs qui sont tout simplement incroyables dans le code qu'ils produisent. Donc, comme réponse générale, il y aura de nombreux endroits où vous pouvez aller dans la sortie du compilateur et modifier l'assembleur pour les performances, et / ou simplement réécrire la routine à partir de zéro.


8
GCC effectue des optimisations extrêmement indépendantes de la plate-forme. Cependant, il n'est pas si bon d'utiliser pleinement des jeux d'instructions particuliers. Pour un tel compilateur portable, il fait un très bon travail.
Artelius

2
D'accord. Sa portabilité, les langues qui entrent et les cibles qui sortent sont incroyables. Le fait d'être portable peut nuire à être vraiment bon dans une langue ou une cible. Ainsi, les opportunités pour un humain de faire mieux sont là pour une optimisation particulière sur une cible spécifique.
old_timer

+1: GCC n'est certainement pas compétitif pour générer du code rapide, mais je ne suis pas sûr que ce soit parce qu'il est portable. LLVM est portable et je l'ai vu générer du code 4x plus rapidement que les GCC.
Jon Harrop

Je préfère GCC, car il est solide depuis de nombreuses années, et il est disponible pour presque toutes les plates-formes pouvant exécuter un compilateur portable moderne. Malheureusement, je n'ai pas pu construire LLVM (Mac OS X / PPC), donc je ne pourrai probablement pas y passer. L'une des bonnes choses à propos de GCC est que si vous écrivez du code qui se construit dans GCC, vous restez probablement proche des normes, et vous serez sûr qu'il peut être construit pour presque n'importe quelle plate-forme.

4

Longpoke, il n'y a qu'une seule limitation: le temps. Lorsque vous n'avez pas les ressources nécessaires pour optimiser chaque changement de code et passer votre temps à allouer des registres, à optimiser quelques déversements et quoi de plus, le compilateur gagnera à chaque fois. Vous faites votre modification du code, recompilez et mesurez. Répétez si nécessaire.

En outre, vous pouvez faire beaucoup de choses sur le côté de haut niveau. En outre, inspecter l'assemblage résultant peut donner l'impression que le code est de la merde, mais en pratique, il s'exécutera plus rapidement que ce que vous pensez serait plus rapide. Exemple:

int y = données [i]; // fais quelques trucs ici .. call_function (y, ...);

Le compilateur lira les données, les poussera à empiler (déversement) et lira plus tard à partir de la pile et passera comme argument. Ça a l'air merdique? Il peut en fait s'agir d'une compensation de latence très efficace et d'une exécution plus rapide.

// version optimisée call_function (data [i], ...); // pas si optimisé après tout ..

L'idée avec la version optimisée était que nous avons réduit la pression du registre et évité les déversements. Mais en vérité, la version "merdique" était plus rapide!

En regardant le code d'assemblage, en regardant simplement les instructions et en concluant: plus d'instructions, plus lentement, serait une erreur de jugement.

La chose ici à faire attention est la suivante: de nombreux experts en montage pensent qu’ils en savent beaucoup, mais en savent très peu. Les règles changent également de l'architecture à la suivante. Il n'y a pas de code Silver-bullet x86, par exemple, qui est toujours le plus rapide. Ces jours-ci, il vaut mieux suivre les règles de base:

  • la mémoire est lente
  • le cache est rapide
  • essayez d'utiliser mieux le cache
  • combien de fois tu vas manquer? avez-vous une stratégie de compensation de latence?
  • vous pouvez exécuter 10 à 100 instructions ALU / FPU / SSE pour un seul échec de cache
  • l'architecture des applications est importante ..
  • .. mais ça n'aide pas quand le problème n'est pas dans l'architecture

En outre, faire trop confiance au compilateur pour transformer comme par magie un code C / C ++ mal pensé en code "théoriquement optimal" est un vœu pieux. Vous devez connaître le compilateur et la chaîne d'outils que vous utilisez si vous vous souciez des "performances" à ce bas niveau.

Les compilateurs en C / C ++ ne sont généralement pas très bons pour réorganiser les sous-expressions car les fonctions ont des effets secondaires, pour commencer. Les langages fonctionnels ne souffrent pas de cette mise en garde mais ne correspondent pas bien à l'écosystème actuel. Il existe des options de compilation pour permettre des règles de précision assouplies qui permettent de modifier l'ordre des opérations par le générateur de compilateur / éditeur de liens / code.

Ce sujet est un peu une impasse; pour la plupart, ce n'est pas pertinent, et le reste, ils savent déjà ce qu'ils font de toute façon.

Tout se résume à ceci: "comprendre ce que vous faites", c'est un peu différent de savoir ce que vous faites.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.