3
votes

Sortie de compilateur complexe pour un constructeur simple

J'ai une structure X avec deux membres entiers 64 bits et un constructeur:

X::X(unsigned long, unsigned long):
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     qword ptr [rbp - 16], rsi
    mov     qword ptr [rbp - 24], rdx
    mov     rdx, qword ptr [rbp - 8]
    mov     qword ptr [rdx], 0
    mov     qword ptr [rdx + 8], 0
    mov     rsi, qword ptr [rbp - 16]
    mov     qword ptr [rdx + 8], rsi
    mov     rsi, qword ptr [rbp - 24]
    mov     qword ptr [rdx], rsi
    pop     rbp
    ret

Quand je regarde la sortie du compilateur (x86-64 gcc 8.3 et x86-64 clang 8.0 .0, sur Linux 64 bits), sans optimisation activée, je vois le code suivant pour le constructeur.

x86-64 gcc 8.3:

X::X(unsigned long, unsigned long):
    push    rbp
    mov     rbp, rsp
    mov     QWORD PTR [rbp-8], rdi
    mov     QWORD PTR [rbp-16], rsi
    mov     QWORD PTR [rbp-24], rdx
    mov     rax, QWORD PTR [rbp-8]
    mov     QWORD PTR [rax], 0
    mov     rax, QWORD PTR [rbp-8]
    mov     QWORD PTR [rax+8], 0
    mov     rax, QWORD PTR [rbp-8]
    mov     rdx, QWORD PTR [rbp-16]
    mov     QWORD PTR [rax+8], rdx
    mov     rax, QWORD PTR [rbp-8]
    mov     rdx, QWORD PTR [rbp-24]
    mov     QWORD PTR [rax], rdx
    nop
    pop     rbp
    ret

x86-64 clang 8.0.0:

struct X
{
    X(uint64_t a, uint64_t b)
    {
        a_ = a; b_ = b;
    }

    uint64_t a_, b_;
};

Est-ce que quelqu'un sait pourquoi la sortie est si complexe? Je me serais attendu à deux simples instructions "mov", même sans optimisation activée.


7 commentaires

a_ = a; b_ = b; n'est pas une initialisation. C'est une mission. Essayez X (uint64_t a, uint64_t b): a_ (a), b_ (b) {}


Si vous désactivez les optimisations, vous ne devez pas vous attendre à un code optimisé.


@NathanOliver dans ce contexte (puisqu'ils sont des int s) est le même.


@Artyer Ce n'est pas l'initialisation contre le corps du constructeur. Les deux versions génèrent ces mêmes mouvements étranges: gcc.godbolt.org/z/PsJVwr .


@Artyer Je ne pense pas que le constructeur par défaut pour les types intégrés puisse être appelé, quelles que soient les optimisations. En effet, l'assemblage généré est le même pour les deux "chemin de construction"


Ouais, ce n'est pas une construction par défaut. Les movs bizarres proviennent d'un this-> implicite. Je n'ai pas pu obtenir le même résultat d'assemblage: godbolt.org/z/NmZBDy


Est-il possible que l'assemblage que vous avez posté ne soit pas, en fait, du code source que vous avez posté? Je n'obtiens la remise à zéro que si j'ajoute des devoirs en classe comme uint64_t a_ = 0;


3 Réponses :


7
votes

Le code non optimisé stocke toujours toutes les variables C ++ (y compris les arguments de fonction) dans leur emplacement mémoire entre les instructions, afin que les valeurs soient disponibles pour que le débogueur puisse les lire et même modifier . (Et parce qu'il n'a pas passé de temps à faire l'allocation de registre.) Cela inclut le stockage des arguments de registre dans la mémoire avant la première instruction C ++ d'une fonction.


Il s'agit d'un assemblage de syntaxe Intel comme de gcc -masm = intel , donc il utilise la destination, l'ordre source. (Nous pouvons le dire sur la base de l'utilisation de PTR, de crochets et de l'absence de % sur les noms de registre.)

Les 3 premiers magasins sont les arguments de fonction (ceci, un , b) qui ont été passés dans les registres RDI, RSI et RDX selon la convention d'appel de l'ABI x86-64 System V.

mov     rax, QWORD PTR [rbp-8]
mov     rdx, QWORD PTR [rbp-16]        # reload a
mov     QWORD PTR [rax+8], rdx         # this->b_ = a
mov     rax, QWORD PTR [rbp-8]
mov     rdx, QWORD PTR [rbp-24]        # reload b
mov     QWORD PTR [rax], rdx           # this->a_ = b

Maintenant, il charge ceci dans rax et écrit des zéros dans a_ et b_ parce que vous n'avez pas utilisé l'initialisation correcte du constructeur. Ou peut-être avez-vous ajouté l'initialisation à zéro avec du code que vous n'avez pas montré ici, ou une option de compilateur étrange.

mov     rax, QWORD PTR [rbp-8]
mov     QWORD PTR [rax], 0           # this->a_ = 0
mov     rax, QWORD PTR [rbp-8]
mov     QWORD PTR [rax+8], 0         # this->b_ = 0

Ensuite, il charge ce code > dans rax à nouveau et a dans rdx , puis écrit this-> a_ avec rdx code> aka a . Encore une fois pour b.

Attendez, en fait cela doit être une écriture dans b_ d'abord puis une écriture dans a_ car les structures doivent correspondre à la déclaration et à l'ordre de la mémoire. Donc [rax + 8] doit être b_ , pas a_.

mov     QWORD PTR [rbp-8], rdi        # this
mov     QWORD PTR [rbp-16], rsi       # a
mov     QWORD PTR [rbp-24], rdx       # b

Donc, votre asm ne correspond pas à la source C ++ de votre question.


2 commentaires

gcc -fverbose-asm commentera l'asm pour vous avec des noms de var C ++ pour les opérandes, par exemple godbolt.org/z/3QNU7v montre ce code compilé par les mêmes compilateurs que dans la question. (Je me demande si c'est de là que l'OP a réellement copié l'asm, car Godbolt vérifie par défaut la case de syntaxe Intel.) Mais comme le mentionne Paul Sanders, gcc8.3 et clang8.0 ne sont pas à zéro a _ et b_ avant d'attribuer les arguments.


Oh, bien vu que le code asm et C ++ de l'OP ne correspondent même pas. Il ne s'agit pas seulement de faire b_ = b avant a_ = a (ce que les compilateurs ne feraient jamais à -O0 ), il fait b_ = a . Ou il a les membres de la structure déclarés dans l'ordre inverse. J'ai commenté les blocs de code dans votre réponse avec ce qui se passe réellement. Et BTW, je suis à peu près sûr que l'asm a été copié à partir de l'explorateur du compilateur Godbolt à cause du X :: X démêlé (unsigned long, unsigned long): name, la syntaxe Intel par défaut et le filtrage des directives .cfi_ * stack-unwind-info et autres métadonnées.



1
votes

Comme d'autres l'ont commenté, le compilateur n'a aucune obligation d'optimiser votre code lorsque vous ne le lui demandez pas, mais une grande partie de l'inefficacité provient de:

  • les paramètres de débordement du compilateur passés dans les registres à une zone d'attente de la pile lors de l'entrée dans la fonction (puis en utilisant les copies sur la pile par la suite)
  • le fait qu'Intel ne dispose d'aucune instruction MOV de mémoire à mémoire

Ces deux facteurs se combinent pour vous donner le code que vous voyez dans le démontage (bien que clang fasse clairement un meilleur travail que gcc ici).

Le compilateur renverse ces registres dans la pile pour faciliter le débogage - parce qu'ils sont sur la pile, les paramètres passés dans la fonction restent disponibles tout au long de la fonction et cela peut être très utile lors du débogage. En outre, vous pouvez jouer des astuces comme la correction de nouvelles valeurs pour les paramètres susmentionnés à un point d'arrêt avant de continuer l'exécution, lorsque vous réalisez ce que leurs valeurs devraient réellement être et que vous voulez continuer votre session de débogage.

Je ne sais pas pourquoi les deux compilateurs mettent à zéro a_ et b_ avant de les leur attribuer lors de votre désassemblage. Je ne vois pas ça chez Godbolt .


0 commentaires

3
votes

Que se passe-t-il et pourquoi?

Si vous n'activez pas les optimisations, le compilateur stocke toutes les variables de la pile , et le compilateur renvoie toutes les valeurs de la pile . La raison pour laquelle il le fait est qu'il permet aux débogueurs de suivre plus facilement ce qui se passe dans le programme: ils peuvent observer la pile du programme.

De plus, chaque fonction doit mettre à jour le pointeur de pile lorsque la fonction est entrée, et réinitialiser le pointeur de pile lorsque la fonction est quittée. C'est aussi pour le bénéfice du débogueur: le débogueur peut toujours dire exactement quand vous entrez une fonction ou quittez une fonction.

Code avec -O0:

X::X(unsigned long, unsigned long):
    mov     rax, rdi
    mov     rdx, rsi
    ret

Code avec -O1 :

X::X(unsigned long, unsigned long):
    mov     rax, rdi
    mov     rdx, rsi
    ret

Est-ce que c'est important?

En quelque sorte. Le code sans optimisations est beaucoup plus lent, en particulier parce que le compilateur doit faire des choses comme ça. Mais il n'y a pratiquement aucune raison ne pas d'activer l'optimisation.

Comment déboguer du code optimisé

gcc et clang ont tous deux l'option -Og : cette option active toutes les optimisations qui ne interfère avec le débogage. Si la version de débogage du code s'exécute lentement, essayez de la compiler avec -Og .

Code avec -Og:

X::X(unsigned long, unsigned long):
    push    rbp        // Push the frame pointer to the stack
    mov     rbp, rsp   // Copy the frame pointer to the rsb register
    // Create the object (on the stack)
    mov     QWORD PTR [rbp-8], rdi  
    mov     QWORD PTR [rbp-16], rsi
    mov     QWORD PTR [rbp-24], rdx
    mov     rax, QWORD PTR [rbp-8]
    mov     rdx, QWORD PTR [rbp-16]
    mov     QWORD PTR [rax], rdx
    mov     rax, QWORD PTR [rbp-8]
    mov     rdx, QWORD PTR [rbp-24]
    mov     QWORD PTR [rax+8], rdx
    nop     // IDEK why it does this
    // Pop the frame pointer
    pop     rbp
    ret

Resources

Plus d'informations sur -Og et d'autres options pour faciliter le débogage du code: https: // gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html

Plus d'informations sur les options d'optimisation et d'optimisation: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options p >


2 commentaires

Je pense que vous voulez dire "mettre à jour le pointeur de cadre" à l'entrée de votre 2ème paragraphe. ( -fno-omit-frame-pointer ). Les débogueurs savent quand les fonctions sont entrées / sorties en fonction de RIP, et non de RSP ou RBP. C'est utile pour les backtraces / déroulement de pile, mais GDB ne revient aux pointeurs de frame pour les backtraces que s'il n'y a pas de section .eh_frame .


Dans Pourquoi clang produit-il une asm inefficace avec -O0 (pour cette simple somme à virgule flottante)? ​​, j'ai expliqué plus en détail pourquoi -O0 codegen est si méchant: c'est ainsi que les débogueurs peuvent modifier n'importe quelle variable lorsqu'ils sont arrêtés à un point d'arrêt entre les instructions, ou même sauter à une ligne source différente dans la même fonction. C'est pourquoi il n'y a pas de propagation constante à travers les variables, par exemple.