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




tag site internet (24)

Réponse courte? Parfois.

Techniquement, toute abstraction a un coût et un langage de programmation est une abstraction du fonctionnement de la CPU. C est cependant très proche. Il y a des années, je me souviens d'avoir ri à haute voix lorsque je me suis connecté à mon compte UNIX et que j'ai reçu le message de fortune suivant (quand ces 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 est intéressant de noter que le langage d'assemblage fonctionne mais que vous l'écrivez. Il existe cependant un compilateur entre C et le langage d'assemblage qu'il génère et c'est extrêmement important car la rapidité de votre code C a énormément à voir avec la qualité de votre compilateur.

Lorsque gcc est apparu sur la scène, l'une des choses qui l'a rendu si populaire était que c'était souvent tellement mieux que les compilateurs C livrés avec beaucoup de saveurs UNIX commerciales. Non seulement c'était ANSI C (aucune de ces ordures de K & R C), mais il était plus robuste et produisait typiquement un code meilleur (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 parce qu'il n'y a pas de standard objectif pour C.

De même, l'assembleur varie beaucoup en fonction du processeur que vous utilisez, des spécifications de votre système, des instructions que vous utilisez et ainsi de suite. Historiquement, il existe deux familles d'architectures CPU: CISC et RISC. Le plus grand joueur de l'ICCA é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 gagné la bataille pour les coeurs et les esprits.

Quoi qu'il en soit, la sagesse populaire quand j'étais un développeur plus jeune était que x86 manuscrit pouvait souvent être beaucoup plus rapide que C parce que la façon dont l'architecture fonctionnait, elle 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, donc personne (je le savais) n'a écrit Sparc assembler. Je suis sûr que de telles personnes existaient mais sans aucun doute elles sont toutes deux devenues fous et ont été institutionnalisées maintenant.

Les jeux d'instructions sont un point important même dans la même famille de processeurs. Certains processeurs Intel ont des extensions telles que 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 de sorte qu'il était optimisé pour le processeur que vous utilisiez. C'était un travail difficile en assembleur.

Il y a toujours des optimisations que vous pouvez faire en assembleur qu'aucun compilateur ne pourrait faire et un algoirthm d'assembleur bien écrit sera aussi rapide ou plus rapide que son équivalent en C. La plus grande question est: est-ce que ça vaut le coup?

En fin de compte, l'assembleur était un produit de son temps et était plus populaire à une époque où les cycles de CPU étaient chers. De nos jours, un processeur qui coûte 5-10 $ à fabriquer (Intel Atom) peut faire à peu près tout ce que n'importe qui peut vouloir. La seule vraie raison pour écrire un 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), des pilotes de périphériques, éventuellement des périphériques embarqués. aussi) et ainsi de suite. Ou juste pour des coups de pied (qui est quelque peu masochiste).

L'une des raisons avancé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, en particulier C. Cependant, j'ai aussi entendu dire à plusieurs reprises que bien que ce ne soit pas entièrement faux, les cas où l'assembleur peut réellement être utilisé pour générer du code plus performant sont extrêmement rares et nécessitent une connaissance et une expérience de l'assemblage.

Cette question ne prend même pas en compte le fait que les instructions de l'assembleur seront spécifiques à la machine et non portables, ou à tout autre aspect de l'assembleur. Bien entendu, il y a de bonnes raisons de se réunir en plus de celle-ci, mais il s'agit d'une question spécifique sollicitant des exemples et des données, et non un discours étendu sur l'assembleur par rapport aux langages supérieurs.

Quelqu'un peut-il fournir des exemples spécifiques de 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 confiant que ces cas existent, mais je veux vraiment savoir exactement comment ces cas sont ésotériques, car il semble être un point de discorde.



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

  1. Multiplication à virgule fixe: Le produit de deux nombres de 16 bits est un nombre de 32 bits. Mais les règles de C disent que le produit de deux nombres de 16 bits est un nombre de 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 que la moitié supérieure d'une multiplication 16x16 ou d'une multiplication 32x32, vous devez jouer à des jeux avec le compilateur. La méthode générale consiste à projeter une largeur de bit plus grande que nécessaire, à la multiplier, à la décaler et à la renvoyer:

    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 essayez simplement d'obtenir la moitié supérieure d'un 16x16 multiplier et faire la bonne chose avec le 16x16 natif de la machine. Ou il peut être stupide et exiger un appel de bibliothèque pour faire la multiplication 32x32 c'est beaucoup trop parce que vous avez seulement besoin de 16 bits du produit - mais le standard C ne vous donne aucun moyen de vous exprimer.

  2. Certaines opérations de bitshifting (rotation / carry):

    // 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, il va faire beaucoup de travail "inutile". De nombreux jeux d'instructions d'assemblage vous permettent de faire pivoter ou de déplacer vers la gauche / droite avec le résultat dans le registre de report, de sorte que vous pouvez accomplir les 34 instructions ci-dessus: charger un pointeur au début du tableau, effacer le report bit à droite, en utilisant l'auto-incrémentation 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 dans l'assemblage: Prenez un morceau de N bits (8, 16, 32, 64, 128, etc.), déplacez le tout à droite algorithme), alors si la valeur résultante est 1 alors vous XOR dans un modèle 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 performance s'accompagne de coûts importants.

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


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

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

tandis que dans 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 en utilisant le mot d'ordre élevé de eax et ebx, il deviendra 8 fois plus rapide que la routine C précédente!


One of the more famous snippets of assembly is from Michael Abrash's texture mapping loop ( expained in detail here ):

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

Nowadays most compilers express advanced CPU specific instructions as intrinsics, ie, functions that get compiled down to the actual instruction. MS Visual C++ supports intrinsics for MMX, SSE, SSE2, SSE3, and SSE4, so you have to worry less about dropping down to assembly to take advantage of platform specific instructions. Visual C++ can also take advantage of the actual architecture you are targetting with the appropriate /ARCH setting.


A few examples from my experience:

  • Access to instructions that are not accessible from C. For instance, many architectures (like x86-64, IA-64, DEC Alpha, and 64-bit MIPS or PowerPC) support a 64 bit by 64 bit multiplication producing a 128 bit result. GCC recently added an extension providing access to such instructions, but before that assembly was required. And access to this instruction can make a huge difference on 64-bit CPUs when implementing something like RSA - sometimes as much as a factor of 4 improvement in performance.

  • Access to CPU-specific flags. The one that has bitten me a lot is the carry flag; when doing a multiple-precision addition, if you don't have access to the CPU carry bit one must instead compare the result to see if it overflowed, which takes 3-5 more instructions per limb; and worse, which are quite serial in terms of data accesses, which kills performance on modern superscalar processors. When processing thousands of such integers in a row, being able to use addc is a huge win (there are superscalar issues with contention on the carry bit as well, but modern CPUs deal pretty well with it).

  • SIMD. Even autovectorizing compilers can only do relatively simple cases, so if you want good SIMD performance it's unfortunately often necessary to write the code directly. Of course you can use intrinsics instead of assembly but once you're at the intrinsics level you're basically writing assembly anyway, just using the compiler as a register allocator and (nominally) instruction scheduler. (I tend to use intrinsics for SIMD simply because the compiler can generate the function prologues and whatnot for me so I can use the same code on Linux, OS X, and Windows without having to deal with ABI issues like function calling conventions, but other than that the SSE intrinsics really aren't very nice - the Altivec ones seem better though I don't have much experience with them). As examples of things a (current day) vectorizing compiler can't figure out, read about bitslicing AES or SIMD error correction - one could imagine a compiler that could analyze algorithms and generate such code, but it feels to me like such a smart compiler is at least 30 years away from existing (at best).

On the other hand, multicore machines and distributed systems have shifted many of the biggest performance wins in the other direction - get an extra 20% speedup writing your inner loops in assembly, or 300% by running them across multiple cores, or 10000% by running them across a cluster of machines. And of course high level optimizations (things like futures, memoization, etc) are often much easier to do in a higher level language like ML or Scala than C or asm, and often can provide a much bigger performance win. So, as always, there are tradeoffs to be made.



Un cas d'utilisation qui ne s'appliquera peut-être plus qu'à votre plaisir de nerd: Sur l'Amiga, le processeur et les puces graphiques / audio se disputeraient pour accéder à une certaine zone de RAM (les 2 premiers Mo de RAM spécifiques). Ainsi, lorsque vous n'aviez que 2 Mo de RAM (ou moins), l'affichage de graphiques complexes et la lecture du son réduisaient les performances du processeur.

En assembleur, vous pouvez entrelacer votre code d'une manière si intelligente que le processeur ne tenterait d'accéder à la RAM que lorsque les puces graphiques / audio seraient occupées en interne (c'est-à-dire lorsque le bus serait libre). Donc, en réordonnant vos instructions, l'utilisation intelligente du cache du processeur, le timing du bus, vous pouviez obtenir des effets qui n'étaient simplement pas possibles en utilisant un langage de niveau supérieur car vous deviez passer chaque commande, même insérer des NOPs ici et là éclate les uns des autres radar.

C'est une autre raison pour laquelle l'instruction NOP (Pas d'Opération - Ne rien faire) du CPU peut réellement accélérer l'exécution de l'ensemble de votre application.

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


I think the general case when assembler is faster is when a smart assembly programmer looks at the compiler's output and says "this is a critical path for performance and I can write this to be more efficient" and then that person tweaks that assembler or rewrites it from scratch.


Given the right programmer, Assembler programs can always be made faster than their C counterparts (at least marginally). It would be difficult to create a C program where you couldn't take out at least one instruction of the Assembler.


You don't actually know whether your well-written C code is really fast if you haven't looked at the disassembly of what compiler produces. Many times you look at it and see that "well-written" was subjective.

So it's not necessary to write in assembler to get fastest code ever, but it's certainly worth to know assembler for the very same reason.


The simple answer... One who knows assembly well (aka has the reference beside him, and is taking advantage of every little processor cache and pipeline feature etc) is guaranteed to be capable of producing much faster code than any compiler.

However the difference these days just doesn't matter in the typical application.


Point un qui n'est pas la réponse.
Même si vous ne programmez jamais dedans, je trouve utile de connaître au moins un ensemble d'instructions assembleur. Cela fait partie de la quête sans fin des programmeurs pour en savoir plus et donc être meilleur. Aussi utile lorsque vous entrez dans les 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 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 périphériques à accès restreint, vous négociez souvent la taille du code / la taille de la mémoire / les performances d'une manière qui serait difficile à donner à un compilateur. Par exemple je sais que cette action de l'utilisateur n'est pas souvent appelée, donc je vais avoir une petite taille de code et une mauvaise performance, mais cette autre fonction similaire est utilisée toutes les secondes pour avoir une taille de code plus grande et des performances plus rapides. C'est le genre de compromis qu'un programmeur qualifié peut utiliser.

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

Mon ami travaille sur des micro-contrô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 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é, pensez-vous

  1. Supprimer une fonctionnalité existante
  2. Réduire la taille de certaines ou de toutes les fonctionnalités existantes peut-être au détriment des performances.
  3. Promouvoir 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 tout 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 alors quand est-ce plus rapide.

alors essayons une fois de plus 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 assembly au compilateur généré pour voir ce qui est plus rapide / plus petit / meilleur.

David.


Matrix operations using SIMD instructions is probably faster than compiler generated code.


Voici un exemple du monde réel: les multiplications de points fixes.

Ceux-ci ne sont pas seulement utiles sur les périphériques sans virgule flottante, ils brillent quand il s'agit de précision car ils vous donnent 32 bits de précision avec une erreur prévisible (float a seulement 23 bits et il est plus difficile de prédire la perte de précision)

Une façon d'écrire un point fixe multiplié par une architecture 32 bits ressemble à ceci:

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é dans le langage C. Nous voulons multiplier deux nombres de 32 bits et obtenir un résultat de 64 bits dont nous renvoyons le 32 bits du milieu. Cependant, en C, cette multiplication n'existe pas. Tout ce que vous pouvez faire est de promouvoir les nombres entiers en 64 bits et de faire une multiplication 64 * 64 = 64.

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

Il nous reste donc un ou deux appels de bibliothèque juste pour une multiplication. Cela a des conséquences graves. Non seulement le décalage est plus lent, mais les registres doivent être conservés à travers les appels de fonction, ce qui n'aide pas non plus à l'inlining et au déroulement du code.

Si vous réécrivez le même code dans l'assembleur, vous pouvez augmenter considérablement la vitesse.

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 de l'assembleur sous forme intrinsèque si vous ne pouvez pas les exprimer en C. Le compilateur VS.NET2008 par exemple expose le mulet 32 ​​* 32 = 64 comme __emul et le décalage de 64 bits comme __ll_rshift.

En utilisant intrinsics, vous pouvez réécrire la fonction de telle sorte que le compilateur C ait une chance de comprendre ce qui se passe. Cela permet d'aligner le code, d'attribuer le registre, d'éliminer la sous-expression commune et de faire une propagation constante. 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 fixe du 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 de points fixes est encore pire. J'ai eu des améliorations jusqu'au facteur 10 pour le code de point fixe lourd de division en écrivant un couple de ASM-lignes.

L'utilisation de Visual C ++ 2013 donne le même code d'assemblage pour les deux manières.


I can't give the specific examples because it was too many years ago, but there were plenty of cases where hand-written assembler could out-perform any compiler. Reasons why:

  • You could deviate from calling conventions, passing arguments in registers.

  • You could carefully consider how to use registers, and avoid storing variables in memory.

  • For things like jump tables, you could avoid having to bounds-check the index.

Basically, compilers do a pretty good job of optimizing, and that is nearly always "good enough", but in some situations (like graphics rendering) where you're paying dearly for every single cycle, you can take shortcuts because you know the code, where a compiler could not because it has to be on the safe side.

In fact, I have heard of some graphics rendering code where a routine, like a line-draw or polygon-fill routine, actually generated a small block of machine code on the stack and executed it there, so as to avoid continual decision-making about line style, width, pattern, etc.

That said, what I want a compiler to do is generate good assembly code for me but not be too clever, and they mostly do that. In fact, one of the things I hate about Fortran is its scrambling the code in an attempt to "optimize" it, usually to no significant purpose.

Usually, when apps have performance problems, it is due to wasteful design. These days, I would never recommend assembler for performance unless the overall app had already been tuned within an inch of its life, still was not fast enough, and was spending all its time in tight inner loops.

Added: I've seen plenty of apps written in assembly language, and the main speed advantage over a language like C, Pascal, Fortran, etc. was because the programmer was far more careful when coding in assembler. He or she is going to write roughly 100 lines of code a day, regardless of language, and in a compiler language that's going to equal 3 or 400 instructions.


It all depends on your workload.

For day-to-day operations, C and C++ are just fine, but there are certain workloads (any transforms involving video (compression, decompression, image effects, etc)) that pretty much require assembly to be performant.

They also usually involve using CPU specific chipset extensions (MME/MMX/SSE/whatever) that are tuned for those kinds of operation.


Ce n'est que lors de l'utilisation de certains jeux d'instructions spéciaux que le compilateur ne prend pas en charge.

Pour optimiser la puissance de calcul d'un processeur moderne avec plusieurs pipelines et branchements prédictifs, vous devez structurer le programme d'assemblage de manière à ce qu'il soit 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 de plus de performances que les micro-optimisations que vous pouvez faire dans l'assemblage.


gcc has become a widely used compiler. Its optimizations in general are not that good. Far better than the average programmer writing assembler, but for real performance, not that good. There are compilers that are simply incredible in the code they produce. So as a general answer there are going to be many places where you can go into the output of the compiler and tweak the assembler for performance, and/or simply re-write the routine from scratch.


I have an operation of transposition of bits that needs to be done, on 192 or 256 bits every interrupt, that happens every 50 microseconds.

It happens by a fixed map(hardware constraints). Using C, it took around 10 microseconds to make. When I translated this to Assembler, taking into account the specific features of this map, specific register caching, and using bit oriented operations; it took less than 3.5 microsecond to perform.


Presque 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 certains numéros de mon PC exécutent 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 limité de la mémoire éclipse d'autres optimisations.

Oups, je courais une version légèrement différente du code et il a sorti les nombres à l'envers (c était plus rapide!). Correction et mise à jour des résultats.


More often than you think, C needs to do things that seem to be unneccessary from an Assembly coder's point of view just because the C standards say so.

Integer promotion, for example. If you want to shift a char variable in C, one would usually expect that the code would do in fact just that, a single bit shift.

The standards, however, enforce the compiler to do a sign extend to int before the shift and truncate the result to char afterwards which might complicate code depending on the target processor's architecture.


Dans mon travail, il y a trois raisons pour moi de connaître et d'utiliser l'assemblage. Par ordre d'importance:

  1. Débogage - Je reçois 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'assemblée. Je dois le faire environ une fois par semaine. Je l'utilise également comme un outil pour déboguer des problèmes dans lesquels mes yeux ne détectent pas l'erreur idiomatique en C / C ++ / C #. Regarder l'assemblée dépasse cela.

  2. Optimisation - le compilateur fait assez bien l'optimisation, mais je joue dans un stade différent de la plupart. J'écris un code de traitement d'image qui commence généralement par du 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 passe 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 de «faire quelque chose», les gains de performance sont considérablement amplifiés. Je ne commence généralement pas par là - je commence généralement par écrire le code pour travailler 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 - obtenant l'architecture de processeur et les caractéristiques spécifiques de processeur, accédant aux drapeaux pas dans le CPU (l'homme, je souhaite vraiment que C te donne l'accès au drapeau de report), etc. Je le fais peut-être une fois par an ou deux ans.


Il y a plusieurs années, j'enseignais à quelqu'un pour programmer en C. L'exercice consistait à faire pivoter un graphique à 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 remanier le problème en utilisant des changements de bits, et le temps de traitement est passé à environ 30 secondes. optimiser le compilateur qu'il avait. Je venais juste d'avoir un compilateur d'optimisation et le même code faisait pivoter le graphique en moins de 5 secondes. J'ai regardé le code d'assemblage que le compilateur générait, et d'après ce que j'ai vu, j'ai décidé que mes jours d'écriture étaient terminés.





assembly