Pourquoi l'initialisation agrégée GCC d'un tableau remplit-elle d'abord le tout avec des zéros, y compris des éléments non nuls?

21

Pourquoi gcc remplit-il le tableau entier avec des zéros au lieu des 96 entiers restants uniquement? Les initialiseurs non nuls sont tous au début du tableau.

void *sink;
void bar() {
    int a[100]{1,2,3,4};
    sink = a;             // a escapes the function
    asm("":::"memory");   // and compiler memory barrier
    // forces the compiler to materialize a[] in memory instead of optimizing away
}

MinGW8.1 et gcc9.2 font tous deux asm comme ceci ( Godbolt compiler explorer ).

# gcc9.2 -O3 -m32 -mno-sse
bar():
    push    edi                       # save call-preserved EDI which rep stos uses
    xor     eax, eax                  # eax=0
    mov     ecx, 100                  # repeat-count = 100
    sub     esp, 400                  # reserve 400 bytes on the stack
    mov     edi, esp                  # dst for rep stos
        mov     DWORD PTR sink, esp       # sink = a
    rep stosd                         # memset(a, 0, 400) 

    mov     DWORD PTR [esp], 1        # then store the non-zero initializers
    mov     DWORD PTR [esp+4], 2      # over the zeroed part of the array
    mov     DWORD PTR [esp+8], 3
    mov     DWORD PTR [esp+12], 4
 # memory barrier empty asm statement is here.

    add     esp, 400                  # cleanup the stack
    pop     edi                       # and restore caller's EDI
    ret

(avec SSE activé, il copierait les 4 initialiseurs avec movdqa load / store)

Pourquoi GCC ne fait-il pas lea edi, [esp+16]et ne met -il pas (avec rep stosd) uniquement les 96 derniers éléments, comme le fait Clang? Est-ce une optimisation manquée, ou est-ce plus efficace de le faire de cette façon? (Clang appelle en fait memsetau lieu de l'inliner rep stos)


Note de l'éditeur: la question avait à l'origine une sortie de compilateur non optimisée qui fonctionnait de la même manière, mais le code inefficace -O0ne prouve rien. Mais il s'avère que cette optimisation est manquée par GCC même à -O3.

Passer un pointeur vers aune fonction non en ligne serait une autre façon de forcer le compilateur à se matérialiser a[], mais en code 32 bits qui conduit à un encombrement important de l'asm. (Les arguments de pile entraînent des push, qui sont mélangés avec des magasins à la pile pour initier le tableau.)

L'utilisation de volatile a[100]{1,2,3,4}GCC permet de créer puis de copier le tableau, ce qui est fou. Normalement, volatilec'est bon pour regarder comment les compilateurs initialisent les variables locales ou les présentent sur la pile.

Gamine
la source
1
@Damien Vous avez mal compris ma question. Je demande pourquoi, par exemple, la valeur a [0] est affectée deux fois comme si a[0] = 0;puis a[0] = 1;.
Lassie
1
Je ne suis pas en mesure de lire l'assemblage, mais où cela montre-t-il que le tableau est entièrement rempli de zéros?
smac89 du
3
Un autre fait intéressant: pour plus d'éléments initialisés, gcc et clang reviennent à copier tout le tableau à partir de .rodata... Je ne peux pas croire que la copie de 400 octets soit plus rapide que la mise à zéro et la définition de 8 éléments.
Jester
2
Vous avez désactivé l'optimisation; un code inefficace n'est pas surprenant jusqu'à ce que vous vérifiiez que la même chose se produit -O3(ce qu'il fait). godbolt.org/z/rh_TNF
Peter Cordes
12
Qu'est-ce que tu veux savoir de plus? C'est une optimisation manquée, allez le signaler sur le bugzilla de GCC avec le missed-optimizationmot - clé.
Peter Cordes

Réponses:

2

En théorie, votre initialisation pourrait ressembler à ça:

int a[100] = {
  [3] = 1,
  [5] = 42,
  [88] = 1,
};

il peut donc être plus efficace en termes de cache et d'optimisation de mettre à zéro tout le bloc de mémoire puis de définir des valeurs individuelles.

Peut être les changements de comportement en fonction de:

  • architecture cible
  • OS cible
  • longueur du tableau
  • rapport d'initialisation (valeurs / longueur explicitement initialisées)
  • positions des valeurs initialisées

Bien sûr, dans votre cas, l'initialisation est compactée au début du tableau et l'optimisation serait triviale.

Il semble donc que gcc fasse l'approche la plus générique ici. On dirait une optimisation manquante.

vlad_tepesch
la source
Oui, une stratégie optimale pour ce code serait probablement de tout mettre à zéro, ou peut-être tout simplement à partir de a[6]maintenant avec les premières lacunes remplies de magasins uniques d'immédiats ou de zéros. Surtout si vous ciblez x86-64, vous pouvez donc utiliser les magasins qword pour faire 2 éléments à la fois, le plus bas étant différent de zéro. par exemple mov QWORD PTR [rsp+3*4], 1pour faire les éléments 3 et 4 avec un magasin qword mal aligné.
Peter Cordes
Le comportement pourrait en théorie dépendre du système d'exploitation cible, mais dans le GCC réel, il ne le sera pas et n'a aucune raison de le faire. Seule l'architecture cible (et à l'intérieur de cela, les options de réglage pour différentes microarchitectures, comme -march=skylakevs -march=k8vs -march=knlseraient toutes très différentes en général, et peut-être en termes de stratégie appropriée pour cela.)
Peter Cordes
Est-ce même autorisé en C ++? Je pensais que c'était seulement C.
Lassie
@Lassie vous avez raison en c ++ ce n'est pas autorisé, mais la question est plus liée au backend du compilateur, de sorte que cela n'a pas beaucoup d'importance. le code affiché peut également être à la fois
vlad_tepesch
Vous pouvez même facilement construire des exemples qui fonctionnent de la même manière en C ++ en déclarant certains struct Bar{ int i; int a[100]; int j;} et initialiser Bar a{1,{2,3,4},4};gcc fait la même chose: zéro tout, puis définissez les 5 valeurs
vlad_tepesch