web-dev-qa-db-fra.com

Pourquoi l'ajout de commentaires d'assemblage en ligne entraîne-t-il un tel changement radical dans le code généré de GCC?

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?

81

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 volatile asm.

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.

58
Matthew Slattery

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.

21
Jester

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é.

4
Mats Petersson