Donc, j'ai eu ce code:
constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
for(int i = 0; i < N; ++i) {
sum[i] = a[i] + b[i];
}
}
void f2(char* sum, char* a, char* b) {
char* end = sum + N;
while(sum != end) {
*sum++ = *a++ + *b++;
}
}
Je voulais voir le code que GCC 4.7.2 générerait. Alors j'ai couru g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11
Et obtenu la sortie suivante:
.file "a.c++"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl _Z2f1PcS_S_
.type _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
.cfi_startproc
lea rcx, [rdx+16]
lea rax, [rdi+16]
cmp rdi, rcx
setae r8b
cmp rdx, rax
setae cl
or cl, r8b
je .L5
lea rcx, [rsi+16]
cmp rdi, rcx
setae cl
cmp rsi, rax
setae al
or cl, al
je .L5
xor eax, eax
.p2align 4,,10
.p2align 3
.L3:
movdqu xmm0, XMMWORD PTR [rdx+rax]
movdqu xmm1, XMMWORD PTR [rsi+rax]
paddb xmm0, xmm1
movdqu XMMWORD PTR [rdi+rax], xmm0
add rax, 16
cmp rax, 992
jne .L3
mov ax, 8
mov r9d, 992
.L2:
sub eax, 1
lea rcx, [rdx+r9]
add rdi, r9
lea r8, [rax+1]
add rsi, r9
xor eax, eax
.p2align 4,,10
.p2align 3
.L4:
movzx edx, BYTE PTR [rcx+rax]
add dl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], dl
add rax, 1
cmp rax, r8
jne .L4
rep
ret
.L5:
mov eax, 1000
xor r9d, r9d
jmp .L2
.cfi_endproc
.LFE0:
.size _Z2f1PcS_S_, .-_Z2f1PcS_S_
.p2align 4,,15
.globl _Z2f2PcS_S_
.type _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
.cfi_startproc
lea rcx, [rdx+16]
lea rax, [rdi+16]
cmp rdi, rcx
setae r8b
cmp rdx, rax
setae cl
or cl, r8b
je .L19
lea rcx, [rsi+16]
cmp rdi, rcx
setae cl
cmp rsi, rax
setae al
or cl, al
je .L19
xor eax, eax
.p2align 4,,10
.p2align 3
.L17:
movdqu xmm0, XMMWORD PTR [rdx+rax]
movdqu xmm1, XMMWORD PTR [rsi+rax]
paddb xmm0, xmm1
movdqu XMMWORD PTR [rdi+rax], xmm0
add rax, 16
cmp rax, 992
jne .L17
add rdi, 992
add rsi, 992
add rdx, 992
mov r8d, 8
.L16:
xor eax, eax
.p2align 4,,10
.p2align 3
.L18:
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, r8
jne .L18
rep
ret
.L19:
mov r8d, 1000
jmp .L16
.cfi_endproc
.LFE1:
.size _Z2f2PcS_S_, .-_Z2f2PcS_S_
.ident "GCC: (GNU) 4.7.2"
.section .note.GNU-stack,"",@progbits
Je suce à la lecture de l'Assemblée, j'ai donc décidé d'ajouter des marqueurs pour savoir où se passaient les corps des boucles:
constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
for(int i = 0; i < N; ++i) {
asm("# im in ur loop");
sum[i] = a[i] + b[i];
}
}
void f2(char* sum, char* a, char* b) {
char* end = sum + N;
while(sum != end) {
asm("# im in ur loop");
*sum++ = *a++ + *b++;
}
}
Et GCC cracher cela:
.file "a.c++"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl _Z2f1PcS_S_
.type _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
.cfi_startproc
xor eax, eax
.p2align 4,,10
.p2align 3
.L2:
#APP
# 4 "a.c++" 1
# im in ur loop
# 0 "" 2
#NO_APP
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, 1000
jne .L2
rep
ret
.cfi_endproc
.LFE0:
.size _Z2f1PcS_S_, .-_Z2f1PcS_S_
.p2align 4,,15
.globl _Z2f2PcS_S_
.type _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
.cfi_startproc
xor eax, eax
.p2align 4,,10
.p2align 3
.L6:
#APP
# 12 "a.c++" 1
# im in ur loop
# 0 "" 2
#NO_APP
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, 1000
jne .L6
rep
ret
.cfi_endproc
.LFE1:
.size _Z2f2PcS_S_, .-_Z2f2PcS_S_
.ident "GCC: (GNU) 4.7.2"
.section .note.GNU-stack,"",@progbits
Ceci est considérablement plus court et présente des différences significatives telles que le manque d'instructions de SIMD. Je m'attendais à la même sortie, avec quelques commentaires quelque part au milieu de cela. Est-ce que je fais une mauvaise hypothèse ici? L'optimiseur de GCC est-il entravé par les commentaires de l'ASM?
Les interactions avec les optimisations sont expliquées à peu près à mi-chemin de la page "Instructions d'assemblage avec des opérandes d'expression C" page dans la documentation.
GCC n'essaie pas de comprendre l'un de l'assemblage réel à l'intérieur de asm
; La seule chose à laquelle il connaît le contenu est ce que vous (éventuellement) le dire dans la spécification de sortie et d'entrée d'entrée et la liste des clobs de registre.
Notamment note:
Une instruction
asm
sans opérandes de sortie sera traitée à l'identique à une instruction volatileasm
.
et
Le mot clé
volatile
indique que l'instruction a des effets secondaires importants [...]
Donc, la présence du asm
à l'intérieur de votre boucle a inhibé une optimisation de vectorisation, car GCC suppose qu'il a des effets secondaires.
Notez que GCC a vectorisé le code, séparant le corps de la boucle en deux parties, le premier traitement de 16 articles à la fois, et le second fait le reste plus tard.
Comme l'a commenté IRA, le compilateur n'utilise pas le bloc ASM. Il ne sait donc pas que ce n'est qu'un commentaire. Même si c'était le cas, il n'a aucun moyen de savoir ce que vous vouliez. Les boucles optimisées ont-elles doublé le corps, si cela peut-il mettre votre ASM dans chacun? Souhaitez-vous que cela ne soit pas exécuté 1000 fois? Cela ne sait pas, donc cela va à la voie de sécurité et retourne à la simple boucle unique.
Je ne suis pas d'accord avec le "GCC ne comprend pas ce qui se trouve dans le bloc asm()
". Par exemple, GCC peut traiter très bien d'optimiser les paramètres et même de réorganiser des blocs asm()
de telle sorte qu'il mêle au code C généré. C'est pourquoi, si vous regardez l'assembleur en ligne dans par exemple, le noyau Linux, il est presque toujours préfixé avec __volatile__
Pour vous assurer que le compilateur "ne déplace pas le code". J'ai eu GCC déplacer mon "RDTSC" autour de ce qui a fait mes mesures du temps qu'il a fallu pour faire certaines choses.
Comme documenté, GCC traite certains types de blocks asm()
en tant que "spécial", et ne optimiste donc pas le code de chaque côté du bloc.
Cela ne veut pas dire que GCC ne sera pas confondu par des blocs d'assembleurs en ligne, ou simplement décider d'abandonner une optimisation particulière car elle ne peut pas suivre les conséquences du code de l'assembleur, etc., etc. Plus important encore, peut souvent être confondu par des tags de clobber manquants - donc si vous avez des instructions telles que cpuid
_ Cela modifie la valeur de EAX-EDX, mais vous avez écrit le code de sorte qu'il utilise uniquement EAX, le compilateur peut stocker des choses dans EBX, ECX et EDX, puis votre code agit très étrange lorsque ces registres sont écrasés ... Si vous avez de la chance, il se bloque immédiatement - il est facile de comprendre ce qui se passe. Mais si vous êtes malchanceux, il se bloque la ligne ... une autre chose délicate est l'instruction de division qui donnent un second résultat à EDX. Si vous ne vous souciez pas du modulo, il est facile d'oublier que EDX a été changé.