Je peux utiliser GCC pour convertir des fichiers de code d'assemblage en fichiers réallouables.
gcc -c source.S -o object.o -O2
L'option d'optimisation est-elle efficace? Puis-je m'attendre à ce que GCC optimise mon code d'assemblage?
3 Réponses :
Non.
GCC transmet votre source d'assemblage via le préprocesseur, puis à l'assembleur. A aucun moment aucune optimisation n'est effectuée.
Qu'en est-il des optimisations de temps de liaison? (pas dans le cas d'OP, mais en général)
@EugeneSh. Que veux-tu dire par là?
@EugeneSh.: Je m'attendrais à ce que la plupart, sinon la totalité, des optimisations au moment de la liaison reposent sur les informations transmises par le compilateur (via des structures de données spéciales dans les fichiers objets ou dans les fichiers qui leur sont associés, similaires aux informations de débogage) ou des structures particulières ou des paramètres dans ou avec le code d'assemblage qui indiquent certaines optimisations, comme l'identification de sous-sections potentiellement séparables par des symboles globaux, sont disponibles.
@EricPostpischil Cela a du sens.
J'ai une idée très vague d'un souvenir d'un langage d'assemblage frontal pour GCC ou Clang, de sorte qu'il lirait l'assembly comme n'importe quel autre langage de programmation, construirait son arbre sémantique interne habituel et ainsi de suite, l'optimiserait et générerait un nouvel assemblage. Mais je ne me souviens pas de détails, même pas si c'était une idée ou si quelqu'un faisait réellement quelque chose à ce sujet ou si cela avait déjà été fait quelque part. (Hmm, on pouvait identifier les entrées d'une routine par laquelle les registres étaient utilisés sans initialisation évidente. Mais les sorties ne pouvaient pas être correctement déterminées automatiquement.)
C'est décevant. Je pensais qu'il y en avait encore beaucoup si des optimisations pouvaient être effectuées lors de la phase d'assemblage, comme les optimisations de niveau d'instruction et l'intégration d'appels, etc.
@willswordpath En fait, cela semble être une bonne chose. Vous pouvez écrire des éléments que vous ne souhaitez pas optimiser dans l'assemblage. Je veux dire quelle est l'autre raison d'écrire en assemblage?
@EugeneSh. Ils peuvent toujours ajouter une option volatile quelque chose comme -fasm-volatile je suppose? Il doit y avoir quelqu'un qui a déjà travaillé sur l'optimisation de la scène d'assemblage quelque part, car c'est un aspect énorme des industries du logiciel.
@willswordpath: La plupart du code n'est pas écrit à la main dans asm. Si vous voulez un ASM optimisé, optimisez-le vous-même de manière à ce que les outils automatiques ne soient pas assez intelligents, ou laissez les compilateurs le générer à partir d'un code source de plus haut niveau. Mais il existe des optimiseurs binaires à binaires qui pourraient, je suppose, être utiles si vous aviez du code produit par un mauvais vieux compilateur qui ne connaissait pas les processeurs modernes. (le code machine et asm sont faciles à traduire). Voir Existe-t-il des compilateurs ASM? . J'ai entendu parler des optimiseurs binaires à binaires. Mais ce n'est pas ce que la plupart des gens veulent que GAS fasse.
@willswordpath Historiquement, il existe des chaînes d'outils qui optimisaient le code d'assemblage. L'un d'eux est la chaîne d'outils Plan 9 qui survit dans les rudiments en tant que partie de la chaîne d'outils Go. Les pièces d'optimisation de l'assemblage ont cependant été découpées; ce n'est plus une approche à la mode. En effet, il est beaucoup plus facile d'optimiser le code avant qu'il ne soit transformé en assemblage et très peu de gens écrivent de toute façon l'assembly.
so.s
ldr r0,=0x12345678 ldr r0,=0x1000 ldr r0,=0xFFFFFF12 00000000 <.text>: 0: e59f0004 ldr r0, [pc, #4] ; c <.text+0xc> 4: e3a00a01 mov r0, #4096 ; 0x1000 8: e3e000ed mvn r0, #237 ; 0xed c: 12345678 .word 0x12345678
Il n'a même pas prétraité la définition.
renommer so.s en so.S
gcc -flto -O2 so.c -save-temps -o so.o cat so.s .file "so.c" .section .gnu.lto_.profile.3f5dbe2a70110b8,"e",@progbits .string "x\234ca`d`a`" .string "\222L\214" .string "" .string "o" .ascii "\016" .text .section .gnu.lto_.icf.3f5dbe2a70110b8,"e",@progbits .string "x\234ca`d" .string "\001\016\006\004`d\330|\356\347Nv\006" .ascii "\017\243\003I" .text .section .gnu.lto_.jmpfuncs.3f5dbe2a70110b8,"e",@progbits .string "x\234ca`d" .string "\001V\006\004" .string "\213" .string "" .string "" .string "\356" .ascii "\f" .text .section .gnu.lto_.inline.3f5dbe2a70110b8,"e",@progbits .string "x\234ca`d" .string "\001\021\006\004" .string "\21203120\001\231l\013\344\231\300b" .string "\n\031" .ascii "\352" .text .section .gnu.lto_.pureconst.3f5dbe2a70110b8,"e",@progbits .string "x\234ca`d`f`" .string "\222\f" .string "" .string "X" .ascii "\n" .text .section .gnu.lto_main.3f5dbe2a70110b8,"e",@progbits .ascii "x\234\035\216\273\016\001a\020\205\347\314\277\313\026\210\236" .ascii "B\253\3610^\301\003(<\300\376\330B\024\262\005\211\210r\223-" .ascii "\334[\3256\n\005\2117\020\n\211NH(\0043&9\2319\231o.\016\201" .ascii "4f\242\264\250 \202!p\270'jz\fha=\220\317\360\361bkp\b\226c\363" .ascii "\344\216`\216\330\333nt\316\251\005Jb/Qo\210rl%\216\233\276\327" .ascii "\r\3211L-\201\247(b\202\242^\230\241L\302\236V\237A6\025([RD" .ascii ":s\244\364\243E5\261\337o\333&q\336e\242\273H\037y0k6W\264\362" .ascii "\272`\033\255\337\031\275\315p\261\370\357\026\026\312\310\204" .ascii "\333\250Wj\364\003\t\210<\r" .text .section .gnu.lto_.symbol_nodes.3f5dbe2a70110b8,"e",@progbits .string "x\234ca`d\020f" .string "\002&\206z\006\206\t\347\030@\324\256\206@\240\b" .ascii "'\370\004\002" .text .section .gnu.lto_.refs.3f5dbe2a70110b8,"e",@progbits .string "x\234ca`\004B " .string "" .string "" .string "9" .ascii "\007" .text .section .gnu.lto_.decls.3f5dbe2a70110b8,"e",@progbits .string "x\234\205PMK\002Q\024\275\347\315h\222\021R-\\\270\020\027\355\222\244\020\367A\355b6A\264\013\261p\221AmZ^\377\200DB\340N\004)\320j~A\bA\021\371\007J!\241e\277@\b\354\276y3\216\320\242\013\367\343\335w\3369\367]\233@\332\372\222V%\357\213O\304\224\344\003\nM\243\\\372k\272g\211/\211\257\210;\377\340\331\302w{\370\025\031\340\035\242\201D\202\022\004xC\350\344\225\306\275\243\024\312\213\024\266\020" .ascii "\375\263\nJ_\332\300u\317\344I`\001\211O\345\253i\006\302tB\363" .ascii "\b\360X\303\247Se\005\337h\226\330\260\316\360\032q\177\023A" .ascii "\224\337\337<\266\027\207\370\2502s\223\331\301T\322[#Q\224\331" .ascii "\326\373\204\2058\321\302S\203\235+\301\266\270\247\367%\004" .ascii "\215\376[\335\262\226\241\353\317\361\355v\266+\327|\311\254" .ascii "\n\341\216;?\265\227x\362Z\337\214\252\234\006\234yl\244\260" .ascii "\236\022\261\007$%\036\331\0069~\346V4\323d\327\345Q\375U\325" .ascii "\270\247GS\032\205;\031\342\036Y=\241\224\022\273\030\002\035" .ascii "\fd`\027\031\232\273(\344\327\362\233\024;.UJg\345\"\331'\207" .ascii "\345Jlgw/\275\225\313Q\344\3744[\244_\320\267k~" .text .section .gnu.lto_.symtab.3f5dbe2a70110b8,"e",@progbits .string "main" .string "" .string "" .string "" .string "" .string "" .string "" .string "" .string "" .string "" .string "" .string "" .string "\260" .string "" .string "" .text .section .gnu.lto_.opts,"e",@progbits .string "'-fmath-errno' '-fsigned-zeros' '-ftrapping-math' '-fno-trapv' '-fno-openmp' '-fno-openacc' '-mtune=generic' '-march=x86-64' '-O2' '-flto' '-fstack-protector-strong'" .text .comm __gnu_lto_v1,1,1 .comm __gnu_lto_slim,1,1 .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits
Il pré-traite la définition mais aucune optimisation ne se produit.
En regardant un peu plus profondément et ce qui est passé comme
gcc -flto -O2 so.S -save-temps -o so.o cat so.s # 1 "so.S" # 1 "<built-in>" # 1 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 1 "<command-line>" 2 # 1 "so.S" mov $0x5, %eax mov $0x5,%eax mov $0x5,%eax mov $0x5,%eax retq
Et
int main ( void ) { return(5); } gcc -O2 so.c -save-temps -o so.o cat so.s .file "so.c" .section .text.unlikely,"ax",@progbits .LCOLDB0: .section .text.startup,"ax",@progbits .LHOTB0: .p2align 4,,15 .globl main .type main, @function main: .LFB0: .cfi_startproc movl $5, %eax ret .cfi_endproc .LFE0: .size main, .-main .section .text.unlikely .LCOLDE0: .section .text.startup .LHOTE0: .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits
toujours pas d'optimisation.
Devrait être plus que suffisant pour démontrer. Il existe des optimisations de temps de liaison que vous pouvez faire, vous devez créer les objets correctement, puis en informer l'éditeur de liens. Mais je soupçonne qu'il ne le fait pas au niveau du code machine mais à un niveau élevé et recrée le code.
gcc -O2 -c -save-temps so.S -o so.o [0][as] [1][--64] [2][-o] [3][so.o] [4][so.s] cat so.s # 1 "so.S" # 1 "<built-in>" # 1 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 1 "<command-line>" 2 # 1 "so.S" mov $0x5, %eax mov $0x5,%eax mov $0x5,%eax mov $0x5,%eax retq
Utilisation du so.S d'en haut
gcc -O2 -c -save-temps so.s -o so.o [0][as] [1][--64] [2][-o] [3][so.o] [4][so.s] cat so.s #define HELLO 0x5 mov $HELLO, %eax mov $0x5,%eax mov $0x5,%eax mov $0x5,%eax retq
Utiliser le so.c d'en haut
gcc -O2 -c so.S -o so.o objdump -d so.o 0000000000000000 <.text>: 0: b8 05 00 00 00 mov $0x5,%eax 5: b8 05 00 00 00 mov $0x5,%eax a: b8 05 00 00 00 mov $0x5,%eax f: b8 05 00 00 00 mov $0x5,%eax 14: c3 retq
Il ne semble donc toujours pas que gcc fasse une optimisation en supprimant ces instructions dupliquées qui n'ont aucun avantage fonctionnel et sont fondamentalement du code mort. Cela montre que gcc pré-traitera le code si le fichier a le .S mais pas si .s (peut expérimenter ou lire les documents sur d'autres .asm?). Ceux-ci ont été exécutés sous linux, gcc est gcc, binutils est binutils, la sensibilité spécifique à l'extension des noms de fichiers peut varier selon l'hôte cible.
L'optimisation du temps de liaison semble être liée au code de haut niveau, car on ne s'attend pas au code du langage assembleur. On s'attend à ce que l'optimisation du temps de liaison soit basée sur le code intermédiaire et non sur le backend.
Nous savons que gcc n'est pas un assembleur, il le transmet simplement même s'il est généré à partir de C, il le transmet donc il aurait besoin d'un analyseur d'assembleur, puis d'une logique pour gérer ce langage pour ensuite choisir les choses à transmettre pour le temps de liaison optimisation.
Vous pouvez en savoir plus sur l'optimisation du temps de liaison et voir s'il existe un moyen de l'appliquer à l'assembleur ... Je suppose que non, mais toute votre question porte sur comment utiliser les outils et comment ils fonctionnent.
L'optimisation du langage d'assemblage n'est pas nécessairement une chose, c'est un peu le point, maintenant il y a des choses pseudo-code pour les pseudo instructions que l'assembleur peut choisir une implémentation optimisée
#define HELLO 0x5 mov $HELLO, %eax mov $0x5,%eax mov $0x5,%eax mov $0x5,%eax retq gcc -O2 -c so.s -o so.o objdump -d so.o 0000000000000000 <.text>: 0: b8 00 00 00 00 mov $0x0,%eax 5: b8 05 00 00 00 mov $0x5,%eax a: b8 05 00 00 00 mov $0x5,%eax f: b8 05 00 00 00 mov $0x5,%eax 14: c3 retq
Mais c'est du pseudo code, donc l'assembleur qui le prend en charge est libre de faire ce qu'il veut. (les assembleurs définissent le langage d'assemblage (pas la cible) donc, par définition, ils peuvent faire ce qu'ils veulent). Sur cette note, l'utilisation d'un compilateur en tant qu'assembleur lorsque la chaîne d'outils a également un assembleur le transforme en un autre langage d'assemblage, car le langage d'assemblage est défini par l'outil. Ainsi, lorsque vous autorisez gcc à prétraiter le code, vous utilisez essentiellement un langage d'assemblage différent de as. Tout comme l'assemblage en ligne pour le compilateur est encore un autre langage d'assemblage. Au moins trois langages d'assemblage par cible pour la chaîne d'outils gnu.
Si vous ne voulez pas optimiser manuellement votre asm, le langage d'assemblage est le mauvais choix de langue source pour vous. Considérez peut-être LLVM-IR si vous voulez quelque chose de semblable à asm mais qui est en fait une entrée pour un compilateur d'optimisation. (Et indépendant de l'ISA.)
Pour être honnête, il existe des recompilateurs / optimiseurs binaires à binaires qui essaient de déterminer les détails de l'implémentation et la logique importante, et d'optimiser en conséquence. (La lecture à partir de la source asm au lieu du code machine serait également possible; asm et le code machine sont faciles à convertir d'avant en arrière et ont un mappage presque 1: 1). Mais ce n'est pas ce que font les assembleurs.
Le travail d'un assembleur consiste normalement à traduire fidèlement ce que vous écrivez en asm. Avoir un outil pour faire cela est nécessaire pour expérimenter pour découvrir ce qui est réellement plus rapide, sans l'ennui d'écrire le code machine réel à la main.
Il est intéressant de GAS, l'assembleur GNU a quelques options d'optimisation limitées pour x86 qui ne sont pas activés par le frontal GCC, même si votre course gcc -O2
. (Vous pouvez exécuter gcc -v ...
pour voir comment le front-end appelle d'autres programmes pour faire le vrai travail, avec quelles options.)
Utilisez gcc -Wa,-Os -O3 foo.c bar.S
pour activer l'optimisation complète de votre C et les optimisations mineures du judas de GAS pour votre asm. (Ou -Wa,-O2
, malheureusement le manuel est faux et -Os
manque certaines des optimisations de -O2
) -Wa,...
passe ...
sur la ligne de commande as
, tout comme -Wl,...
passe les options de l'éditeur de liens via l'interface GCC.
GCC n'active normalement pas les optimisations de as
car il alimente normalement le système ASM déjà optimisé de GAS.
Les optimisations de GAS ne concernent que des instructions uniques isolées , et donc uniquement lorsqu'une instruction peut être remplacée par une autre qui a exactement le même effet architectural (à l'exception de la longueur, donc l'effet sur RIP diffère). L'effet micro- architecturale (performance) peut également être différent; c'est le but des optimisations sans taille.
De la as(1)
page de manuel , donc noter que ce sont as
options, non gcc
options.
-O0 | -O | -O1 | -O2 | -Os
Optimisez l'encodage des instructions avec une taille d'instruction plus petite.
-O
et-O1
codent les instructions de chargement de registre 64 bits avec 64 bits immédiat comme instructions de chargement de registre 32 bits avec 31 bits ou 32 bits immédiats, codent les instructions d'effacement de registre 64 bits
avec des instructions d'effacement de registre 32 bits, encoder des instructions d'effacement de registre vectoriel VEX / EVEX 256 bits / 512 bits avec des instructions d'effacement de registre vectoriel VEX 128 bits, encoder des instructions de chargement / stockage de registre vectoriel EVEX 128 bits / 256 bits avec VEX les instructions de chargement / stockage de registre vectoriel, et codent des instructions logiques d'entiers compressés EVEX 128 bits / 256 bits avec des entiers condensés VEX 128 bits / 256 bits logiques.
-O2
inclut l'optimisation-O1
plus encode des instructions d'effacement de registre vectoriel EVEX 256 bits / 512 bits avec des instructions d'effacement de registre vectoriel EVEX 128 bits. En mode 64 bits, les instructions codées en VEX avec des opérandes de source commutative verront également leurs opérandes de source échangés si cela permet d'utiliser la forme de préfixe VEX à 2 octets au lieu de celle à 3 octets. Certaines formes de ET ainsi que OU avec le même opérande (registre) spécifié deux fois seront également changées en TEST.
-Os
inclut-O2
optimisation plus encode 16 bits, 32 bits et
Tests de registre 64 bits avec test immédiat de registre 8 bits avec
immédiat.-O0
désactive cette optimisation.
(re: certaines de ces optimisations de taille d'opérande et de taille de code VEX / EVEX: la mise à zéro de vxorps sur AMD Jaguar / Bulldozer / Zen est-elle plus rapide avec des registres xmm que ymm? et la section vers la fin de ma réponse sur les longueurs d'instructions re: Préfixes VEX 2 contre 3 octets)
Malheureusement, les conflits -O2
et -Os
et -Os
n'incluent pas réellement tout ce qui vient de -O2
. Vous ne pouvez pas l'obtenir pour optimiser test [re]dx, 1
pour test dl,1
( -Os
) et optimiser or al,al
pour test al,al
( -O2
).
test r/m32, imm8
n'est pas encodable donc la version edx a besoin d'un imm32.
or al,al
est un idiome 8080 obsolète qui n'est pas utile pour x86 , sauf parfois sur la famille P6 pour éviter les blocages de lecture de registre où la réécriture intentionnelle du registre est en fait préférable d'éviter d'allonger la chaîne dep.
0000000000000000 <.text>: .intel_syntax noprefix shufps xmm0, xmm0, 0 # -msse2avx just for fun 0: c5 f8 c6 c0 00 vshufps xmm0,xmm0,xmm0,0x0 vxorps zmm31, zmm31, zmm31 # avoids triggering AVX512 frequency limit 5: 62 01 04 00 57 ff vxorps xmm31,xmm31,xmm31 vxorps zmm1, zmm1, zmm1 # shorter, using VEX b: c5 f0 57 c9 vxorps xmm1,xmm1,xmm1 vxorps ymm15, ymm15, ymm15 # missed optimization, could vxorps xmm15, xmm0, xmm0 for a 2-byte VEX and still be a zeroing idiom f: c4 41 00 57 ff vxorps xmm15,xmm15,xmm15 vpxord zmm15, zmm15, zmm15 # AVX512 mnemonic optimized to AVX1, same missed opt for source operands. 14: c4 41 01 ef ff vpxor xmm15,xmm15,xmm15 vpxord ymm3, ymm14, ymm15 # no optimization possible 19: c4 c1 0d ef df vpxor ymm3,ymm14,ymm15 vpxord ymm3, ymm4, ymm15 # reversed operands to allow 2-byte VEX 1e: c5 85 ef dc vpxor ymm3,ymm15,ymm4 vmovd xmm16, [rdi + 256] # uses EVEX scaled disp8 because xmm16 requires EVEX anyway 22: 62 e1 7d 08 6e 47 40 vmovd xmm16,DWORD PTR [rdi+0x100] vmovd xmm0, [rdi + 256] # could use EVEX scaled disp8 but doesn't even with a -march enabling AVX512 29: c5 f9 6e 87 00 01 00 00 vmovd xmm0,DWORD PTR [rdi+0x100] xor rax, rax # dropped REX prefix 31: 31 c0 xor eax,eax or al,al 33: 84 c0 test al,al cmp dl, 0 # optimization to test dl,dl not quite legal: different effect on AF 35: 80 fa 00 cmp dl,0x0 test rdx, 1 # partial optimization: only to 32-bit, not 8-bit 38: f7 c2 01 00 00 00 test edx,0x1 mov rax, 1 3e: b8 01 00 00 00 mov eax,0x1 mov rax, -1 # sign-extended imm8 required 43: 48 c7 c0 ff ff ff ff mov rax,0xffffffffffffffff mov rax, 0xffffffff80000000 4a: 48 c7 c0 00 00 00 80 mov rax,0xffffffff80000000 .att_syntax movabs $-1, %rax # movabs forces imm64, despite -O2 51: 48 b8 ff ff ff ff ff ff ff ff movabs rax,0xffffffffffffffff movq $1, %rax # but explicit q operand size doesn't stop opt 5b: b8 01 00 00 00 mov eax,0x1 movabs $1, %rax 60: 48 b8 01 00 00 00 00 00 00 00 movabs rax,0x1
Assemblé avec gcc -g -Wa,-msse2avx -Wa,-O2 -Wa,-march=znver2+avx512dq+avx512vl -c foo.s
(Pour une raison insensée, as
a fait -march=
prise en charge des processeurs AMD modernes, mais pour Intel uniquement jusqu'à corei7
et certains Xeon Phi, pas Skylake-avx512 comme le fait GCC.J'ai donc dû activer AVX512 manuellement pour tester cela.
objdump -dwrC -Mintel -S
source + démontage
.intel_syntax noprefix shufps xmm0, xmm0, 0 vxorps zmm31, zmm31, zmm31 vxorps zmm1, zmm1, zmm1 vxorps ymm15, ymm15, ymm15 vpxord zmm15, zmm15, zmm15 vpxord ymm3, ymm14, ymm15 vpxord ymm3, ymm4, ymm15 vmovd xmm16, [rdi + 256] # can use EVEX scaled disp8 vmovd xmm0, [rdi + 256] # could use EVEX scaled disp8 but doesn't even with a -march enabling AVX512 xor rax, rax or al,al cmp dl, 0 test rdx, 1 mov rax, 1 mov rax, -1 mov rax, 0xffffffff80000000 .att_syntax movabs $-1, %rax movq $1, %rax movabs $1, %rax
Donc, malheureusement, même l'activation explicite de AVX512VL et AVX512DQ n'a pas permis à GAS de choisir un encodage EVEX plus court pour vmovd
alors qu'un EVEX n'était pas déjà nécessaire. C'est peut-être encore intentionnel: vous voudrez peut-être que certaines fonctions utilisent AVX512, d'autres pour l'éviter. Si vous utilisez les limites d'option ISA pour détecter l'utilisation accidentelle des extensions ISA, vous devrez activer AVX512 pour l'ensemble d'un tel fichier. Il peut être surprenant de trouver l'assembleur utilisant EVEX là où vous ne vous attendiez pas.
Vous pouvez le forcer manuellement avec {evex} vmovd xmm0, [rdi + 256]
. (Qui , malheureusement , CCG ne le fait pas lors de la compilation C, où -march=skylake-avx512
ne certainement donner libre cours à utiliser AVX512 instructions partout.)
Que s'est-il passé lorsque vous avez essayé? Lorsque vous avez examiné l'entrée et la sortie?
Oui j'ai remarqué quelques petits changements entre la source et le démontage. Mais je ne sais pas si c'est à des fins d'optimisation.
Par exemple, les sources
lgdt gdtdesc \n movl %cr0,%eax \n orl $0x1,%eax
ont été traduites en cinq instructionslgdtl (%esi) \n insb (%dx),%es:(%edi) \n jl <addr> \n and %al,%al \n or $0x1,%ax
@old_timer Après un moment, j'ai pensé que cela pouvait être un problème d'affichage d'un désassembleur que les deux codes aient la même expression binaire. Le binaire des instructions disas: 0f 01 16 6c 7c 0f 20 c0 66 83 c8 01
et quand vous avez utilisé comme au lieu de gcc vous avez vu quelque chose de différent?
et quand vous n'avez pas utilisé -O2, vous avez vu quelque chose de différent?
aucun gcc n'optimise asm, ld pourrait mais vous devez préparer les objets correctement
Maintenant, je suis presque sûr que c'est un problème d'affichage. J'ai utilisé -O0 et cela m'a donné le même résultat.
Pas gcc en soi, mais il n'y a aucune garantie qu'un optimiseur de temps de liaison de nouvelle génération; ou plus correctement un optimiseur inter-module, pourrait ne pas avoir un aperçu de votre code et le réparer pour vous.