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

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

cette question n'entre même pas dans le fait que les instructions de montage seront spécifiques à la machine et non portatives, ou aucun des autres aspects de l'assembleur. Il y a beaucoup de bonnes raisons de savoir assembler en dehors de celle-ci, bien sûr, mais ceci est censé être une question spécifique sollicitant des exemples et des données, pas un discours étendu sur assembleur versus langages de niveau supérieur.

quelqu'un Peut-il fournir quelques des exemples précis des cas où l'assemblage sera plus rapide que le code C bien écrit en utilisant un compilateur moderne, et pouvez-vous soutenir cette affirmation avec des preuves de profilage? Je suis assez sûr que ces cas existent, mais je veux vraiment savoir exactement comment esoteric ces cas sont, car il semble être un point de discorde.

407
demandé sur Adam Bellaire 2009-02-23 16:03:26

30 réponses

voici un exemple du monde réel: multiplicateurs de points fixes sur de vieux compilateurs.

ceux-ci ne sont pas seulement utiles sur les appareils sans point flottant, ils brillent quand il s'agit de précision car ils vous donnent 32 bits de précision avec une erreur prévisible (flottant a seulement 23 bits et il est plus difficile de prédire la perte de précision). c'est-à-dire une précision uniforme absolue sur toute la gamme, au lieu d'une précision proche de l'uniformité relative précision ( float ).


les compilateurs modernes optimisent bien cet exemple à point fixe, donc pour les exemples plus modernes qui ont encore besoin d'un code spécifique au compilateur, voir


C n'a pas d'opérateur de multiplication complète (résultat 2N-bit des entrées N-bit). La façon habituelle de l'exprimer en C est de lancer les entrées vers le type plus large et d'espérer que le compilateur reconnaît que le 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 exprimé directement dans le langage C. Nous voulons multiplier deux nombres 32 bits et obtenir un résultat 64 bits dont nous retournons le milieu 32 bits. Toutefois, en C, cette multiplication n'existe pas. Tout ce que vous pouvez faire est de promouvoir les entiers de 64 bits et faire un 64*64 = 64 multiplier.

x86 (et ARM, MIPS et autres) peut toutefois faire le multiplier en une seule instruction. Certains compilateurs utilisés pour ignorer ce fait et générer du code qui appelle une fonction de bibliothèque runtime pour faire la multiplication. Le passage par 16 est également souvent fait par une routine de bibliothèque (x86 pouvez faire ces changements).

donc il nous reste un ou deux appels de bibliothèque juste pour une multiplication. Ce qui a de graves conséquences. Non seulement le décalage est plus lent, mais les registres doivent être préservés à travers les appels de fonction et cela n'aide pas à la mise en ligne et au déroulement du code. soit.

si vous réécrivez le même code dans (inline) assembler vous pouvez gagner une augmentation de vitesse significative.

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

utilisant intrinsics vous pouvez réécrire la fonction de manière à ce que le compilateur ait une chance de comprendre ce qui se passe. Cela permet au code d'être inligné, le registre attribué, l'élimination commune de la sous-expression et la propagation constante peuvent être faites aussi bien. Vous obtiendrez une énorme amélioration des performances par rapport au code assembleur écrit à la main de cette façon.

pour référence: le résultat final pour le mul de point fixe pour le VS.NET le compilateur est:

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

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


en utilisant Visual C++ 2013 donne le même code d'assemblage pour les deux sens.

gcc4.1 de 2007 optimise également la version pure C bien. (L'Explorateur de compilateurs Godbolt n'a pas de versions précédentes de gcc installées, mais probablement même les anciennes versions de GCC pourraient le faire sans être intrinsèques.)

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


les CPU modernes peuvent faire des choses C n'a pas d'opérateurs pour du tout , comme popcnt ou bit-scan pour trouver le premier ou le dernier bit à . (POSIX a une fonction ffs() , 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 la compiler à une instruction popcnt (si activée au moment de la compilation), mais il est beaucoup plus fiable d'utiliser __builtin_popcnt dans GNU C, ou sur x86 si vous ne visez que le matériel avec SSE4.2: _mm_popcnt_u32 from <immintrin.h> .

Ou en C++, de lui attribuer un std::bitset<32> et l'utilisation .count() . (Il s'agit d'un cas où le langage a trouvé un moyen d'exposer de manière exploitable une implémentation optimisée de popcount à travers la bibliothèque standard, d'une manière qui compilera toujours quelque chose de correct, et pourra profiter de tout ce que la cible supporte.) Voir aussi https://en.wikipedia.org/wiki/Hamming_weight#Language_support .

de même, ntohl peut compiler en bswap (x86 32 octets swap pour conversion endian) sur certaines implémentations C qui l'ont.


La vectorisation manuelle avec des instructions SIMD est un autre domaine important pour les asm intrinsèques ou écrits à la main. Les compilateurs ne sont pas mauvais avec des boucles simples comme dst[i] += src[i] * 10.0; , mais font souvent mal ou pas auto-vectoriser 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.

238
répondu Nils Pipenbrinck 2018-07-15 03:23:14

il y a de nombreuses années, j'apprenais à quelqu'un à 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 multiples et des divisions, etc.

je lui ai montré comment refondre 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 d'obtenir un compilateur d'optimisation et le même code a fait tourner le graphique en moins de 5 secondes. J'ai regardé le code d'assemblage que le compilateur générait, et de ce que j'ai vu décidé là-bas et puis que mes jours d'écriture assembleur étaient finis.

120
répondu Peter Cordes 2018-07-25 14:19:35

à peu près à chaque fois que le compilateur voit du code à virgule flottante, une version écrite à la main sera plus rapide. La raison principale est que le compilateur ne peut effectuer aucune optimisation robuste. voir cet article de MSDN pour une discussion sur le sujet. Voici un exemple où la version d'assemblage 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 numéros de mon PC exécutant une compilation de 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 dans le temps - parfois plus rapide, parfois plus lent. Je suppose que l'aspect limité de la mémoire éclipse les autres optimisations.

Oups, j'ai du courir un peu différente de la version du code, et il achera les chiffres à l'envers (c'est à dire C était plus rapide!). Corrigés et mis à jour les résultats.

58
répondu Skizz 2012-07-03 13:52:15

sans donner aucun exemple spécifique ou preuve de profiler, vous pouvez écrire un meilleur assembleur que le compilateur quand 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 le pipeline de processeur fonctionne, il peut essayer de réorganiser les instructions plus rapidement qu'un humain peut, et ainsi de suite-il est fondamentalement la même chose qu'un ordinateur étant aussi bon ou mieux que le meilleur joueur humain pour jeux de société, etc. tout simplement parce qu'il peut faire des recherches dans l'espace de problème plus rapidement que la plupart des humains. Bien que vous puissiez théoriquement effectuer 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 infaisable pour plus de quelques cas (i.e. le compilateur sera très certainement plus performant que vous si vous essayez d'écrire plus que quelques routines dans assembleur).

, d'autre part, il existe des cas où le compilateur n'a pas autant d' information-je dirais principalement quand on 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é avec la connaissance intime d'un humain du matériel en question peut donner de meilleurs résultats qu'un compilateur C pourrait le faire.

D'autres ont mentionné des instructions spéciales, ce qui est ce que je parle dans le paragraphe ci-dessus-instructions dont le compilateur pourrait avoir connaissance limitée ou pas du tout, ce qui permet à un humain d'écrire du code plus rapidement.

52
répondu Liedman 2009-02-23 13:17:06

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

  1. Débogage - je reçois souvent de la bibliothèque de code qui a des bugs ou incomplète de la documentation. Je trouve ce qu'il fait en intervenant au niveau de l'Assemblée. Je dois le faire une fois par semaine environ. Je l'utilise aussi comme un outil pour déboguer les problèmes dans lesquels mes yeux ne repèrent pas l'erreur idiomatique dans C/C++/C#. En regardant l'assemblée obtient passé.

  2. optimisation-le compilateur fait assez bien dans l'optimisation, mais je joue dans un ballpark différent que la plupart. J'écris du code de traitement d'image qui commence habituellement avec du code qui ressemble à ceci:

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

    la "partie de faire quelque chose" se produit typiquement sur l'ordre de plusieurs millions de fois (c'est-à-dire entre 3 et 30). En grattant les cycles dans cette phase de" faire quelque chose", les gains de performance sont considérablement amplifiés. Je n'ai pas l'habitude commencer par là - je commence habituellement par écrire le code pour travailler en premier, puis faire de mon mieux pour reformuler le C pour être naturellement meilleur (meilleur algorithme, moins de charge dans la boucle, etc). Habituellement, j'ai besoin de lire l'assemblée pour voir ce qu'il se passe et rarement besoin de l'écrire. Je le fais peut-être tous les deux ou trois mois.

  3. faire quelque chose que le langage ne me laissera pas faire. Ceux - ci comprennent-l'obtention de l'architecture du processeur et des caractéristiques spécifiques du processeur, l'accès aux Drapeaux pas dans le CPU (man, j'aimerais vraiment que C Vous donne accès au port du drapeau), etc. Je le fais peut-être une fois par an ou de deux ans.

43
répondu plinth 2013-07-21 00:51:26

seulement lorsque l'on utilise des instructions spéciales, le compilateur ne supporte pas.

pour maximiser la puissance de calcul d'un CPU moderne avec plusieurs pipelines et branchement prédictif, vous devez structurer le programme d'assemblage d'une manière qui rend a) presque impossible pour un humain d'écrire b) encore plus impossible à maintenir.

en outre, de meilleurs algorithmes, des structures de données et la gestion de la mémoire vous donnera au moins un ordre de fois plus de performances que les micro-optimisations que vous pouvez faire dans l'assemblée.

40
répondu Nir 2009-02-23 14:07:33

bien que C soit "proche" de la manipulation de bas niveau des données 8-bit, 16-bit, 32-bit, 64-bit, Il y a quelques opérations mathématiques non supportées par C qui peuvent souvent être effectuées élégamment dans certains ensembles d'instructions d'assemblage:

  1. multiplication à point fixe: le produit de deux nombres de 16 bits est un nombre de 32 bits. Mais les règles en C dit que le produit de deux nombres 16 bits en 16 bits, et le produit de deux nombres de 32 bits est un nombre de 32 bits -- la moitié inférieure dans les deux cas. Si vous voulez le haut la moitié d'un 16x16 multiplier ou un 32x32 multiplier, vous devez jouer à des jeux avec le compilateur. La méthode générale consiste à couler sur une largeur de bits plus grande que nécessaire, à multiplier, à dévier vers le bas et à rejeter:

    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 assez intelligent pour savoir que vous êtes vraiment en train d'essayer d'obtenir la moitié supérieure d'un 16x16 multiplier et faire la bonne chose avec le machine natif 16x16multiply. Ou il peut être stupide et exiger un appel de bibliothèque pour faire le 32x32 multiplier qui est beaucoup trop car vous n'avez besoin que de 16 bits du produit -- mais la norme C ne vous donne aucun moyen de vous exprimer.

  2. Certains bitshifting opérations (rotation/porte):

    // 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 soit assez intelligent pour réaliser ce que vous faites, c'est va faire beaucoup de "inutiles" les travaux. De nombreux ensembles d'instructions d'assemblage vous permettent de tourner ou de basculer gauche / droite avec le résultat dans le registre carry, de sorte que vous pourriez accomplir le ci-dessus dans 34 instructions: charger un pointeur au début du tableau, effacer le carry, et effectuer 32 8-bit à droite-shifts, en utilisant auto-incrément sur le pointer.

    pour un autre exemple, il y a registres de rétroaction linéaire (LFSR) qui sont élégamment exécutés dans assemblage: prendre un morceau de N bits (8, 16, 32, 64, 128, etc), déplacez le tout droit par 1 (voir l'algorithme ci-dessus), puis si le carry résultant est 1 alors vous XOR dans un motif bit 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 performance vient avec certains des coûts importants.

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

38
répondu Jason S 2009-03-24 13:48:23

courte réponse? Parfois.

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

Le Langage De Programmation C -- Une langue qui combine le flexibilité du langage de montage avec le pouvoir du langage de l'Assemblée.

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

il est intéressant de noter que le langage assembleur fonctionne comme vous l'écrivez. Il y a cependant un compilateur entre C et le langage d'assemblage qu'il génère et c'est extrêmement important car la vitesse de votre code C a énormément à voir avec la qualité de votre compilateur.

quand gcc est venu sur la scène une des choses qui l'a rendu si populaire était qu'il était souvent tellement mieux que les compilateurs C qui expédiaient avec de nombreuses saveurs UNIX commerciales. Non seulement il était ANSI C (aucun de ces déchets K&R C), était plus robuste et typiquement produit Meilleur (plus rapide) code. Pas toujours, mais souvent.

je vous dis tout cela parce qu'il n'y a pas de règle générale concernant la vitesse de C et assembleur parce qu'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 et ainsi de suite. Historiquement, il y a eu deux familles d'architecture CPU: le CISC et le RISC. Le plus grand acteur du CISC était et est encore L'architecture Intel x86 (et le jeu d'instructions). RISC domine le monde UNIX (MIPS6000, Alpha, Sparc et ainsi de suite). Le CISC a gagné la bataille pour les cœurs et les esprits.

quoi qu'il en soit, la sagesse populaire quand je était un développeur plus jeune était que Main-écrit x86 pouvait souvent être beaucoup plus rapide que C parce que la façon dont l'architecture a fonctionné, il avait une complexité qui a bénéficié d'un humain le faire. RISC d'un autre côté semblait conçu pour les compilateurs de sorte que personne (je savais) écrit dire assembleur Sparc. Je suis sûr que ces gens existaient, mais ils sont tous les deux devenus fous et ont été internés.

jeux d'Instructions sont un point important, même dans la même famille de processeurs. Certains processeurs Intel ont des extensions comme SSE à travers 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 de sorte qu'elle était optimisée pour n'importe quel processeur que vous utilisiez. C'était un travail difficile en monteur.

il y a encore des optimisations que vous pouvez faire en assembleur qu'aucun compilateur ne pourrait faire et un assembleur bien écrit algoirthm sera aussi rapide ou plus rapide que son équivalent en C. Le plus grand la question est: est-il utile?

finalement bien que l'assembleur était un produit de son temps et était plus populaire à une époque où les cycles CPU étaient coûteux. De nos jours, un CPU qui coûte 5 à 10 $pour fabriquer (Intel Atom) peut faire à peu près tout ce que n'importe qui pourrait vouloir. La seule vraie raison d'écrire assembleur ces jours-ci 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 écrit en C), les pilotes de périphérique, peut-être intégré les dispositifs (bien que C ait tendance à dominer là aussi) et ainsi de suite. Ou juste pour s'amuser (ce qui est quelque peu masochiste).

23
répondu cletus 2011-04-03 20:53:33

un cas d'utilisation qui pourrait ne pas s'appliquer plus mais pour votre plaisir nerd: sur L'Amiga, le CPU et les puces graphiques/audio se battraient pour accéder à une certaine zone de RAM (les 2 premiers Mo de RAM à être spécifique). Ainsi, lorsque vous avez seulement 2 Mo de RAM (ou moins), l'affichage de graphiques complexes plus le son de jeu tuerait la performance du CPU.

en assembleur, vous pourriez interférer votre code d'une manière si intelligente que le CPU n'essaierait d'accéder à la RAM que lorsque le les puces graphiques/audio étaient occupées à l'interne (c.-à-d. lorsque le bus était libre). Donc en réordonnant vos instructions, en utilisant intelligemment le cache CPU, le timing du bus, vous pourriez obtenir certains effets qui n'étaient tout simplement pas possibles en utilisant un langage de niveau supérieur parce que vous deviez chronométrer chaque commande, même insérer des NOPs ici et là pour garder les différentes puces hors du radar les uns des autres.

ce qui est une autre raison pour laquelle l'instruction NOP (No Operation - do nothing) de la CPU peut effectivement faites fonctionner votre application entière 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 beaucoup de jeux Amiga ne pouvaient pas faire face à des processeurs plus rapides: le timing des instructions était mauvais.

15
répondu Aaron Digulla 2009-02-23 16:34:50

point un qui n'est pas la réponse.

Même si vous n'y programmez jamais, je trouve utile de connaître au moins un jeu d'instructions assembleur. Cela fait partie de la quête sans fin des programmeurs d'en savoir plus et donc d'être meilleur. Aussi utile lorsque vous marchez dans des cadres, vous n'avez pas le code source et avoir 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 à assembleur.

pour répondre À la question quand vous avez une petite quantité de code ou une grande quantité de temps. Très utile pour une utilisation dans les 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. En outre, pour les appareils restreints, vous échangez souvent la taille du code / la taille de la mémoire / les performances d'une manière qui serait difficile de donner des instructions à un compilateur de faire. par exemple, je sais que cette action de l'utilisateur n'est pas appelée souvent de sorte que je vais avoir la petite taille du code et pauvre performance, mais cette autre fonction qui semble similaire est utilisé chaque seconde donc je vais avoir une plus grande taille de code et des performances plus rapides. C'est le genre de compromis qu'un programmeur d'assemblage qualifié peut utiliser.

je voudrais aussi ajouter Il ya beaucoup de terrain du milieu où vous pouvez code en C compiler et examiner l'assemblage produit, puis soit changer votre code C ou modifier et maintenir comme l'assemblage.

Mon ami travaille sur des micro-contrôleurs, actuellement jetons pour le contrôle de petits moteurs électriques. Il travaille dans une combinaison de bas niveau c et D'assemblage. Une fois, il m'a parlé d'une bonne journée au travail où il a réduit la boucle principale de 48 instructions à 43. Il est également confronté à des choix comme le code a grandi pour remplir la puce 256k et l'entreprise veut une nouvelle fonctionnalité ,do you

  1. Supprimer une caractéristique existante
  2. réduire la taille de tout ou partie des fonctions existantes peut-être au prix de performance.
  3. préconisent de passer à une puce plus grande avec un coût plus élevé, une consommation d'énergie plus élevée et un facteur de forme plus grande.

" je voudrais ajouter comme un développeur commercial avec tout à fait un portfolio ou des langues, des plates-formes, des types d'applications, je n'ai jamais ressenti le besoin de plonger dans l'écriture de l'assemblage. J'ai aussi toujours apprécié les connaissances que j'ai acquises à ce sujet. Et parfois débogué.

je sais que je ont beaucoup plus répondu à la question "Pourquoi devrais-je apprendre assembleur" mais je sens que c'est une question plus importante que quand est-il plus rapide.

essayons encore une fois Vous devriez penser à l'assemblage

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

Rappeler comparer votre assemblage au compilateur généré pour voir lequel est le plus rapide/plus petit / meilleur.

David.

15
répondu David Waters 2011-04-05 20:25:51

je suis surpris que personne n'ait dit ça. La fonction strlen() est beaucoup plus rapide si elle est écrite en assembly! En C, la meilleure chose que vous pouvez faire est

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

pendant le montage 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 penser en utilisant le mot d'ordre haut de eax et ebx, il deviendra 8 fois plus rapide que le précédent C la routine!

14
répondu BlackBear 2017-11-20 09:43:41

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

  • vous pouvez vous écarter des conventions d'appel, passer des arguments dans les registres.

  • vous pourriez examiner attentivement comment utiliser les registres, et éviter de stocker des variables dans la mémoire.

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

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

en fait, je ont entendu parler de certains graphismes rendant le code où une routine, comme une ligne-draw ou polygon-fill routine, effectivement généré un petit bloc de code machine sur la pile et exécuté là, afin d'éviter la prise de décision continue sur le style de ligne, la largeur, le modèle, etc.

cela dit, ce que je veux qu'un compilateur fasse, c'est générer un bon code d'assemblage pour moi, mais pas être trop intelligent, et c'est ce qu'ils font le plus souvent. En fait, une des choses que je déteste chez Fortran c'est son brouillage du code dans une tentative de "l'optimiser", généralement sans but important.

généralement, lorsque les applications ont des problèmes de performance, c'est en raison de la conception gaspilleuse. Ces jours-ci, je ne recommanderais jamais assembleur pour la performance à moins que l'application globale avait déjà été réglé dans un pouce de sa vie, n'était toujours pas assez rapide, et passait tout son temps dans les boucles intérieures serrées.

ajouté: j'ai vu beaucoup d'applications écrites dans le langage d'assemblage, et la vitesse principale avantage sur un langage comme C, Pascal, Fortran,etc. parce que le programmeur a été beaucoup plus prudent lors du codage en assembleur. Il ou elle va écrire environ 100 lignes de code par jour, indépendamment de la langue, et dans un langage de compilateur qui va égaler 3 ou 400 instructions.

13
répondu Mike Dunlavey 2009-02-23 14:25:11
Les opérations matricielles

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

12
répondu Mehrdad Afshari 2009-02-23 13:06:09

boucles serrées, comme en jouant avec des images, depuis une image peut cosist de millions de pixels. S'asseoir et de comprendre comment utiliser au mieux le nombre limité de registres du processeur peut faire une différence. Voici un échantillon de la vraie vie:

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

alors les processeurs ont souvent des instructions ésotériques qui sont trop spécialisées pour qu'un compilateur s'en soucie, mais à l'occasion, un programmeur assembleur peut en faire bon usage. Prenez L'instruction XLAT par exemple. Vraiment génial si vous avez besoin de faire des recherches de table dans une boucle et la table est limitée à 256 octets!

mise à Jour le: Oh, venez à penser de ce qui est le plus crucial quand nous parlons de boucles en général: le compilateur a souvent aucune idée sur le nombre d'itérations qui sera le cas le plus courant! Seul le programmeur sait qu'une boucle sera itérée plusieurs fois et qu'il sera donc bénéfique de préparer la boucle avec un peu de travail supplémentaire, ou si elle sera itérée si peu de fois que la configuration prendra en fait plus de temps que les itérations prévues.

10
répondu Dan Byström 2009-09-05 02:39:40

quelques exemples tirés de mon expérience:

  • accès aux instructions qui ne sont pas accessibles à partir de C. par exemple, de nombreuses architectures (comme x86-64, IA-64, DEC Alpha, et 64-bit MIPS ou PowerPC) 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 cette assemblée était nécessaire. Et l'accès à cette instruction peut faire une énorme différence sur 64 bits CPU lors de la mise en œuvre de quelque chose comme RSA - parfois autant qu'un facteur de 4 Amélioration de la performance.

  • accès aux pavillons propres au CPU. Celui qui m'a beaucoup mordu est le flag carry; quand vous faites un ajout de précision multiple, si vous n'avez pas accès au bit de carry CPU, il faut plutôt comparer le résultat pour voir s'il a débordé, ce qui prend 3-5 plus d'instructions par membre; et pire, qui sont tout à fait série en termes d'accès aux données, ce qui tue la performance sur les processeurs superscalaires modernes. Lors du traitement de milliers de tels entiers dans une rangée, être capable d'utiliser addc est une énorme victoire (il y a des problèmes superscalaires avec la contention sur le bit de portage aussi bien, mais les CPU modernes traitent assez bien avec elle).

  • SIMD. Même les compilateurs autovectorizing ne peuvent faire que des cas relativement simples, donc si vous voulez de bonnes performances SIMD, il est malheureusement souvent nécessaire d'écrire le code directement. Bien sûr vous pouvez utiliser intrinsics au lieu de assembler mais une fois que vous êtes au niveau intrinsics vous écrivez assembly de toute façon, en utilisant simplement le compilateur comme un allocator de registre et (nominalement) instruction scheduler. (J'ai tendance à utiliser intrinsics pour SIMD tout simplement parce que le compilateur peut générer les prologues de fonction et autres pour moi afin que je puisse utiliser le même code sur Linux, OS X, et Windows sans avoir à traiter des questions ABI comme les conventions d'appel de fonction, mais à part cela, la SSE intrinsics vraiment ne sont pas très sympa - les Altivec semblent mieux bien que je n'ai pas beaucoup d'expérience avec eux). Comme exemples de choses qu'un compilateur vectorisant (de nos jours) ne peut pas comprendre, lisez à propos de bitslicing AES ou SIMD error correction - on pourrait imaginer un compilateur qui pourrait analyser des algorithmes et générer un tel code, mais il me semble qu'un tel compilateur intelligent est à au moins 30 ans d'existence (au mieux).

d'un autre côté, les machines multicore et les systèmes distribués ont déplacé beaucoup des plus grandes victoires de performance dans l'autre direction - obtenez une accélération supplémentaire de 20% en écrivant vos boucles internes dans l'assemblage, ou 300% en les exécutant à travers plusieurs noyaux, ou 10000% en les exécutant à travers un faisceau de machines. Et bien sûr les optimisations de haut niveau (des choses comme les futurs, la memoization, etc) sont souvent beaucoup plus faciles à faire dans un langage de haut niveau comme ML ou Scala que C ou asm, et souvent peut fournir une performance beaucoup plus grande victoire. Donc, comme toujours, il y a des compromis à faire.

10
répondu Jack Lloyd 2009-10-15 17:07:57

plus souvent que vous ne le pensez, C a besoin de faire des choses qui semblent être inutiles du point de vue D'un codeur D'assemblage juste parce que les normes C le disent.

entier promotion, par exemple. Si vous souhaitez modifier une variable char en C, attendez que le code ne fait juste que, un simple décalage de bits.

les normes, cependant, obligent le compilateur à faire un signe étendre à int avant le changement et tronquer le résultat à char ensuite qui peut compliquer le code en fonction de l'architecture du processeur cible.

10
répondu mfro 2014-03-15 13:41:19

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

donc il n'est pas nécessaire d'écrire en assembleur pour obtenir le code le plus rapide jamais, mais il est certainement intéressant de connaître assembleur pour la même raison.

9
répondu sharptooth 2009-02-24 04:50:22

je pense que le cas général quand 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 la performance et je peux écrire ceci pour être plus efficace " et puis cette personne modifie que l'assembleur ou réécrit à partir de zéro.

8
répondu Doug T. 2009-02-23 13:11:08

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 un assemblage pour être performant.

ils impliquent aussi généralement l'utilisation D'extensions de chipset spécifiques CPU (MME/MMX/SSE/whatever) qui sont accordés pour ce genre d'opération.

7
répondu Larry Osterman 2009-02-24 04:58:27

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

il se produit par une carte fixe(contraintes matérielles). En utilisant C, il a fallu environ 10 microsecondes pour le faire. Lorsque j'ai traduit ceci à Assembleur, en tenant compte des caractéristiques spécifiques de cette carte, de la mise en cache de Registre spécifique, et en utilisant des opérations orientées bit; il a fallu moins de 3,5 microsecondes pour effectuer.

6
répondu SurDin 2009-05-24 15:28:46

LInux assembly howto , pose cette question et donne le pour et le contre de l'utilisation de assembly.

5
répondu pseudosaint 2009-02-23 15:50:00

la réponse simple... Celui qui sait assemblage puits (aka a la référence à côté de lui, et tire avantage de chaque petite cache de processeur et les caractéristiques de pipeline etc) est garanti d'être capable de produire du code beaucoup plus rapide que n'importe quel" compilateur .

cependant la différence ces jours-ci n'a pas d'importance dans l'application typique.

5
répondu Longpoke 2010-01-21 16:38:22

l'une des posibilités de la version CP/M-86 de PolyPascal (frère de Turbo Pascal) était de remplacer la fonction" use-bios-to-output-characters-to-the-screen " par une routine de langage machine qui à essense donnait le x, Le y et la chaîne à y mettre.

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

il y avait de la place dans le binaire pour intégrer le code machine (quelques centaines d'octets) et il y avait d'autres choses là aussi, il était essentiel de serrer le plus possible.

il s'avère que puisque l'écran était de 80x25 les deux coordonnées pouvaient tenir dans un octet chacun, donc les deux pouvaient tenir dans un mot de deux octets. Cela a permis de faire les calculs nécessaires en moins d'octets depuis une simple pourrait 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 sur eux et diviser ils ressortent à nouveau plus tard (et je ne pense pas que les instructions de la machine seront plus courtes de toute façon).

4
répondu Thorbjørn Ravn Andersen 2009-02-23 14:15:01

L'un des plus célèbres brins d'assemblage est tiré de la boucle de cartographie de la texture de Michael Abrash ( expliqué 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 de CPU avancées comme intrinsèques, c'est-à-dire des fonctions qui sont compilées jusqu'à l'instruction réelle. MS Visual C++ supporte les intrinsèques MMX, SSE, SSE2, SSE3 et SSE4, donc vous devez vous soucier moins de descendre à l'assemblage pour profiter de plate-forme d'instructions spécifiques. Visual C++ peut également profiter de l'architecture actuelle que vous ciblez avec le paramètre approprié /ARCH.

4
répondu MSN 2009-02-23 16:17:19

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

4
répondu Beep beep 2009-02-23 16:24:53

http://cr.yp.to/qhasm.html a beaucoup d'exemples.

4
répondu Vincent 2009-02-23 16:27:11

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

4
répondu old_timer 2009-05-24 15:14:32

Longpoke, il n'y a qu'une limite: le temps. Lorsque vous n'avez pas les ressources pour optimiser chaque changement de code et passer votre temps à allouer des registres, optimiser quelques déversements et ce qui ne l'est pas, le compilateur gagnera chaque fois. Vous effectuez votre modification du code, recompiler et mesurer. Répétez l'opération si nécessaire.

aussi, vous pouvez faire beaucoup dans le côté de haut niveau. En outre, l'inspection de l'ensemble résultant peut donner l'IMPRESSION que le code est de la merde, mais dans la pratique, il va courir plus vite que ce que vous pensez serait plus rapide. Exemple:

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

le compilateur Lira les données, les poussera à la pile (spill) et plus tard les lira à partir de la pile et les passera comme argument. Les sons de merde? Il pourrait en fait être compensation de latence très efficace et entraîner 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 éviter le débordement. Mais en vérité, la version" merdique " était plus rapide!

regarder le code de l'assemblage, juste regarder les instructions et conclure: plus d'instructions, plus lent, serait une erreur de jugement.

la chose ici à faire attention est: beaucoup d'experts de l'Assemblée penser ils savent beaucoup, mais savent très peu de choses. Les règles changent aussi d'une architecture à l'autre. Il n'y a pas de code x86 silver-bullet, par exemple, qui est toujours le plus rapide. Ces jours-ci il vaut mieux suivre les règles du pouce:

  • la mémoire est lente
  • cache est rapide
  • essayez d'utiliser la mise en cache mieux
  • combien de fois allez-vous manquer? avez-vous une stratégie de compensation de latence?
  • vous pouvez exécuter 10-100 Alu/FPU / SSE instructions pour un seul défaut de cache
  • l'architecture d'application est importante..
  • .. mais il n'aide pas quand le problème n'est pas dans l'architecture

aussi, faire trop confiance au compilateur transformer 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 de "performance" à ce de bas niveau.

les compilateurs en C / C++ ne sont généralement pas très bons pour réordonner les sous-expressions parce que les fonctions ont des effets secondaires, pour commencer. Les langages fonctionnels ne souffrent pas de cette mise en garde mais ne correspondent pas à l'écosystème actuel. Il y a des options de compilateur pour permettre des règles de précision assouplies qui permettent à l'ordre des opérations d'être modifié par le compilateur/linker/générateur de code.

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

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

4
répondu tiredcoder 2010-09-17 13:12:59

Que Diriez-vous de créer du code machine à l'exécution?

mon frère a réalisé une fois (vers 2000) un traceur de rayons en temps réel extrêmement rapide en générant du code à l'exécution. Je ne me souviens pas des détails, mais il y avait une sorte de module principal qui était en boucle à travers les objets, puis il préparait et exécutait un code machine qui était spécifique à chaque objet.

cependant, au fil du temps, cette méthode a été devancée par de nouveaux matériel graphique, et il est devenu inutile.

Aujourd'hui, je pense que peut-être certaines opérations sur les big-data (des millions de dossiers) comme les tables de pivotement, le forage, les calculs à la volée, etc. pourrait être optimisée avec cette méthode. La question Est: est-ce que l'effort en vaut la peine?

4
répondu user872744 2015-04-19 10:26:15

j'ai lu toutes les réponses (plus de 30) et n'ai pas trouvé une raison simple: assembleur est plus rapide que C si vous avez lu et pratiqué le Intel® 64 et la-32 Architectures optimisation Manuel de référence , donc la raison pour laquelle l'assemblage peut être plus lent est que les gens qui écrivent l'assemblage plus lent n'a pas lu le manuel D'optimisation .

dans le bon vieux temps D'Intel 80286, chaque instruction a été exécutée à un fixe nombre de cycles CPU, mais depuis Pentium Pro, sorti en 1995, les processeurs Intel sont devenus superscalaires, utilisant des Pipelinages Complexes: exécution hors-ordre & renommage de Registre. Avant cela, sur Pentium, produit 1993, il y avait U et V pipelines: lignes de conduite double qui pourrait 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 hors D'exécution & Registre renommer apparu dans Pentium Pro, et presque laissé inchangé aujourd'.

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

vous pouvez en savoir plus sur L'exécution des ordres et le changement de nom du Registre si le temps le permet, il y a beaucoup d'informations disponibles sur Internet.

Il ya aussi d'autres questions importantes comme la prédiction de branche, le nombre d'unités de charge et de stockage, le nombre de portes qui exécutent des micro-ops, etc, mais la chose la plus importante à considérer est à savoir l'exécution hors-de-L'ordre.

la plupart des gens ne sont tout simplement pas au courant de L'exécution hors ordre, donc ils écrivent leurs programmes d'assemblage comme pour 80286, s'attendant à ce que leur instruction prendra un temps fixe à exécuter quel que soit le contexte; tandis que les compilateurs C sont au courant de l'exécution hors ordre et générez le code correctement. C'est pourquoi le code de ces gens ignorent est plus lent, mais si vous devenez conscient, votre code sera plus rapide.

4
répondu Maxim Masiutin 2017-06-30 18:37:07