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.
3 Réponses :
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.
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 _ code> 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.
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:
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 .
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
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.
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
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 >
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.
a_ = a; b_ = b;
n'est pas une initialisation. C'est une mission. EssayezX (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/NmZBDyEst-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;