187 votes

Le langage d'assemblage en ligne est-il plus lent que le code C++ natif ?

J'ai essayé de comparer les performances du langage d'assemblage en ligne et du code C++, j'ai donc écrit une fonction qui additionne deux tableaux de taille 2000 pour 100000 fois. Voici le code :

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}

void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

Voici main() :

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

Ensuite, j'exécute le programme cinq fois pour obtenir les cycles du processeur, qui peuvent être considérés comme du temps. A chaque fois, j'appelle une seule des fonctions mentionnées ci-dessus.

Et voilà le résultat.

Fonction de la version d'assemblage :

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

Fonction de la version C++ :

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

Le code C++ en mode release est presque 3,7 fois plus rapide que le code assembleur. Pourquoi ?

Je suppose que le code assembleur que j'ai écrit n'est pas aussi efficace que ceux générés par GCC. Cela veut-il dire que je ne dois pas me fier aux performances du langage assembleur que j'ai écrit de mes mains, me concentrer sur le C++ et oublier le langage assembleur ?

30 votes

A peu près. L'assemblage codé à la main est approprié dans certaines circonstances, mais il faut s'assurer que la version assembleur est effectivement plus rapide que ce qui peut être réalisé avec un langage de plus haut niveau.

162 votes

Vous pourriez trouver instructif d'étudier le code généré par le compilateur, et essayer de comprendre pourquoi il est plus rapide que votre version assembleur.

35 votes

Ouais, on dirait que le compilateur est meilleur que toi pour écrire du asm. Les compilateurs modernes sont vraiment très bons.

274voto

Adriano Repetti Points 22087

Oui, la plupart du temps.

Tout d'abord, vous partez de l'hypothèse erronée qu'un langage de bas niveau (assembleur dans ce cas) produira toujours un code plus rapide qu'un langage de haut niveau (C++ et C dans ce cas). Ce n'est pas vrai. Le code C est-il toujours plus rapide que le code Java ? Non, car il existe une autre variable : le programmeur. La façon dont vous écrivez le code et la connaissance des détails de l'architecture influencent grandement les performances (comme vous l'avez vu dans ce cas).

Vous pouvez toujours produire un exemple où le code d'assemblage fait à la main est meilleur que le code compilé mais généralement il s'agit d'un exemple fictif ou d'une routine unique, pas d'une vrai de 500.000+ lignes de code C++). Je pense que les compilateurs produiront un meilleur code d'assemblage 95% du temps et parfois, seulement quelques rares fois, vous pouvez avoir besoin d'écrire du code d'assemblage pour quelques, courts, très utilisé , performance critique ou lorsque vous devez accéder à des fonctionnalités que votre langage de haut niveau préféré n'expose pas. Voulez-vous une touche de cette complexité ? Lisez cette réponse géniale ici sur SO.

Pourquoi cela ?

Tout d'abord parce que les compilateurs peuvent réaliser des optimisations que nous ne pouvons même pas imaginer (cf. cette courte liste ) et ils les feront en secondes (lorsque nous pouvons avoir besoin de jours ).

Lorsque vous codez en assembleur, vous devez créer des fonctions bien définies avec une interface d'appel bien définie. Cependant elles peuvent prendre en compte optimisation de l'ensemble du programme y optimisation interprocédurale comme comme allocation de registres , propagation constante , élimination des sous-expressions communes , planification des instructions et d'autres optimisations complexes et peu évidentes ( Modèle de polytope par exemple). Sur RISC Les spécialistes de l'architecture ont cessé de s'en préoccuper il y a de nombreuses années (l'ordonnancement des instructions, par exemple, est très difficile à réaliser). accorder à la main ) et moderne CISC Les processeurs ont de très longues pipelines aussi.

Pour certains microcontrôleurs complexes, même système sont écrites en C plutôt qu'en assembleur parce que leurs compilateurs produisent un meilleur code final (et plus facile à maintenir).

Les compilateurs peuvent parfois utiliser automatiquement certaines instructions MMX/SIMDx par eux-mêmes, et si vous ne les utilisez pas, vous ne pouvez tout simplement pas comparer (les autres réponses ont déjà très bien examiné votre code assembleur). Juste pour les boucles, c'est un courte liste d'optimisations de boucles de ce qui est communément vérifiés par un compilateur (pensez-vous pouvoir le faire vous-même lorsque votre emploi du temps a été décidé pour un programme C# ?) Si vous écrivez quelque chose en assembleur, je pense que vous devez prendre en compte au moins une partie de ce que vous pouvez faire. optimisations simples . L'exemple classique des tableaux est le suivant dérouler le cycle (sa taille est connue au moment de la compilation). Faites-le et exécutez à nouveau votre test.

De nos jours, il est également très rare d'avoir besoin d'utiliser le langage assembleur pour une autre raison : le pléthore de processeurs différents . Voulez-vous les soutenir tous ? Chacun d'entre eux a une microarchitecture et quelques jeux d'instructions spécifiques . Ils ont un nombre différent d'unités fonctionnelles et les instructions d'assemblage doivent être organisées de manière à ce qu'elles soient toutes conservées. occupé . Si vous écrivez en C, vous pouvez utiliser PGO mais en assemblage, vous aurez alors besoin d'une grande connaissance de cette architecture spécifique (et repenser et tout refaire pour une autre architecture ). Pour les petites tâches, le compilateur généralement le fait mieux, et pour les tâches complexes généralement le travail n'est pas remboursé (et compilateur mai faire mieux de toute façon).

Si vous vous asseyez et que vous jetez un coup d'œil à votre code, vous verrez probablement que vous gagnerez plus à revoir la conception de votre algorithme qu'à le traduire en assembleur (lire ceci un grand poste ici sur SO ), il existe des optimisations de haut niveau (et des indications au compilateur) que vous pouvez appliquer efficacement avant de devoir recourir au langage d'assemblage. Il est probablement utile de mentionner que souvent, en utilisant des intrinsèques, vous obtiendrez le gain de performance que vous recherchez et le compilateur sera toujours capable d'effectuer la plupart de ses optimisations.

Cela dit, même si vous pouvez produire un code d'assemblage 5~10 fois plus rapide, vous devez demander à vos clients s'ils préfèrent payer une semaine de votre temps ou à acheter un processeur 50$ plus rapide . Le plus souvent, l'optimisation extrême (et surtout dans les applications LOB) n'est tout simplement pas nécessaire pour la plupart d'entre nous.

0 votes

@drhirsch C'est déchirant de vous entendre :), mais je sais ce qui est mon travail et ce qui ne l'est pas. Merci à tous !

1 votes

Le compilateur est donc meilleur pour écrire du code que les gens. Bon à savoir.

10 votes

Bien sûr que non. Je pense que c'est mieux pour 95% des gens dans 99% des cas. Parfois parce que c'est tout simplement trop coûteux (à cause de complexe les mathématiques) ou le temps passé (puis coûteux à nouveau). Parfois parce que nous avons tout simplement oublié les optimisations...

194voto

hirschhornsalz Points 16306

Votre code d'assemblage est sous-optimal et peut être amélioré :

  • Vous poussez et poussez un registre ( EDX ) dans votre boucle interne. Elle doit être déplacée hors de la boucle.
  • Vous rechargez les pointeurs du tableau à chaque itération de la boucle. Ceci devrait être déplacé hors de la boucle.
  • Vous utilisez le loop l'instruction, qui est connu pour être très lent sur la plupart des CPU modernes (peut-être le résultat de l'utilisation d'un ancien livre d'assemblée*)
  • Vous ne tirez aucun avantage du déroulement manuel des boucles.
  • Vous n'utilisez pas les ressources disponibles SIMD des instructions.

Donc, à moins que vous n'amélioriez considérablement vos compétences en matière d'assembleur, cela n'a pas de sens pour vous d'écrire du code en assembleur pour les performances.

*Bien sûr, je ne sais pas si vous avez vraiment obtenu le loop les instructions d'un ancien livre d'assemblée. Mais vous ne le voyez presque jamais dans le code réel, car tous les compilateurs sont suffisamment intelligents pour ne pas émettre loop vous ne le voyez que dans des livres mauvais et dépassés.

0 votes

Les compilateurs peuvent toujours émettre loop (et de nombreuses instructions "dépréciées") si vous optimisez la taille.

1 votes

@phuclv bien oui, mais la question originale était exactement sur la vitesse, pas la taille.

60voto

Matthieu M. Points 101624

Avant même de se plonger dans l'assemblage, il existe des transformations de code à un niveau supérieur.

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

peut être transformé en via Rotation de la boucle :

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

qui est bien meilleur en ce qui concerne la localisation de la mémoire.

On peut encore l'optimiser en faisant a += b X fois, cela revient à faire a += X * b donc on obtient :

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

Cependant, il semble que mon optimiseur préféré (LLVM) n'effectue pas cette transformation.

[modifier] J'ai constaté que la transformation s'effectue si nous avions l'option restrict qualificatif pour x y y . En effet, sans cette restriction, x[j] y y[j] pourrait s'aliaser au même endroit, ce qui rend cette transformation erronée. [fin d'édition]

De toute façon, ce est, je pense, la version C optimisée. Elle est déjà beaucoup plus simple. Sur cette base, voici mon essai d'ASM (j'ai laissé Clang le générer, je suis nul en la matière) :

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

J'ai bien peur de ne pas comprendre d'où viennent toutes ces instructions, cependant vous pouvez toujours vous amuser et essayer de voir comment ça se compare... mais j'utiliserais toujours la version C optimisée plutôt que celle en assembleur, en code, beaucoup plus portable.

0 votes

Merci pour votre réponse. Eh bien, c'est un peu confus que lorsque j'ai pris le cours intitulé "Principes du compilateur", j'ai appris que le compilateur va optimiser notre code par de nombreux moyens. Cela signifie-t-il que nous devons optimiser notre code manuellement ? Pouvons-nous faire un meilleur travail que le compilateur ? C'est la question qui me perturbe toujours.

2 votes

@user957121 : nous pouvons mieux l'optimiser lorsque nous avons plus d'informations. Spécifiquement ici, ce qui gêne le compilateur est l'éventuel crénelage entre x y y . C'est-à-dire que le compilateur ne peut pas être sûr que pour toutes les i,j sur [0, length) nous avons x + i != y + j . S'il y a chevauchement, l'optimisation est impossible. Le langage C a introduit le restrict pour indiquer au compilateur que deux pointeurs ne peuvent pas s'aligner, mais cela ne fonctionne pas pour les tableaux car ils peuvent toujours se chevaucher même s'ils ne s'alignent pas exactement.

0 votes

GCC et Clang actuels auto-vectorisent (après avoir vérifié le non-overlap si vous omettez __restrict ). SSE2 est la référence pour x86-64, et avec le shuffling SSE2 peut faire 2x multiplications 32-bit en une fois (produisant des produits 64-bit, d'où le shuffling pour remettre les résultats ensemble). godbolt.org/z/r7F_uo . (SSE4.1 est nécessaire pour pmulld : packed 32x32 => multiplicateur 32 bits). GCC a une astuce pour transformer les multiplicateurs d'entiers constants en shift/add (et/ou soustraction), ce qui est bon pour les multiplicateurs avec peu de bits. Le code lourd du shuffle de Clang va provoquer un goulot d'étranglement sur le débit du shuffle sur les CPU Intel.

41voto

Oli Charlesworth Points 148744

Réponse courte : oui.

Longue réponse : oui, sauf si vous savez vraiment ce que vous faites et que vous avez une raison de le faire.

3 votes

Et seulement si vous avez utilisé un outil de profilage au niveau de l'assemblage comme vtune pour les puces intel pour voir où vous pouvez améliorer certaines choses.

1 votes

Cela répond techniquement à la question mais est aussi complètement inutile. Un -1 de ma part.

2 votes

Réponse très longue : "Oui, à moins que vous n'ayez envie de changer tout votre code à chaque fois qu'un nouveau (er) CPU est utilisé. Choisissez le meilleur algorithme, mais laissez le compilateur se charger de l'optimisation".

35voto

sasha Points 295

J'ai corrigé mon code asm :

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

Résultats pour la version Release :

 Function of assembly version: 81
 Function of C++ version: 161

Le code assembleur en mode release est presque 2 fois plus rapide que le C++.

18 votes

Maintenant, si vous commencez à utiliser SSE au lieu de MMX (le nom du registre est xmm0 au lieu de mm0 ), vous obtiendrez un gain de vitesse supplémentaire d'un facteur deux ;-)

8 votes

J'ai changé, j'ai obtenu 41 pour la version d'assemblage. Il est en 4 fois plus rapide :)

3 votes

Vous pouvez également obtenir jusqu'à 5% de plus si vous utilisez tous les registres xmm

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X