Warum füllt die GCC-Aggregatinitialisierung eines Arrays das Ganze zuerst mit Nullen, einschließlich Nicht-Null-Elementen?

21

Warum füllt gcc das gesamte Array mit Nullen anstatt nur mit den verbleibenden 96 Ganzzahlen? Die Nicht-Null-Initialisierer befinden sich alle am Anfang des Arrays.

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 und gcc9.2 machen beide asm ( 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

(Wenn SSE aktiviert ist, werden alle 4 Initialisierer mit movdqa load / store kopiert.)

Warum macht GCC nicht nur die letzten 96 Elemente lea edi, [esp+16]und memset (mit rep stosd), wie es Clang tut? Ist dies eine verpasste Optimierung oder ist es irgendwie effizienter, dies auf diese Weise zu tun? (Clang ruft tatsächlich an, memsetanstatt zu inlinieren rep stos)


Anmerkung des Herausgebers: Die Frage hatte ursprünglich eine nicht optimierte Compilerausgabe, die auf die gleiche Weise funktionierte, aber ineffizienter Code -O0beweist nichts. Es stellt sich jedoch heraus, dass diese Optimierung von GCC auch bei verfehlt wird -O3.

Das Übergeben eines Zeigers an aeine Nicht-Inline-Funktion wäre eine weitere Möglichkeit, den Compiler zum Materialisieren zu zwingen a[], jedoch in 32-Bit-Code, was zu einer erheblichen Unordnung des ASM führt. (Stapelargumente führen zu Pushs, die mit Speichern im Stapel gemischt werden, um das Array zu initiieren.)

Mit using volatile a[100]{1,2,3,4}wird GCC erstellt, um das Array zu erstellen und dann zu kopieren , was verrückt ist. Normalerweise volatileist es gut zu sehen, wie Compiler lokale Variablen initiieren oder auf dem Stapel auslegen.

Lassie
quelle
1
@ Damien Du hast meine Frage falsch verstanden. Ich frage, warum zum Beispiel der a [0] zweimal der Wert zugewiesen wird, als ob a[0] = 0;und dann a[0] = 1;.
Lassie
1
Ich kann die Assembly nicht lesen, aber wo wird angezeigt, dass das Array vollständig mit Nullen gefüllt ist?
smac89
3
Eine weitere interessante Tatsache: Bei mehr initialisierten Elementen kehren sowohl gcc als auch clang zum Kopieren des gesamten Arrays von .rodata... zurück. Ich kann nicht glauben, dass das Kopieren von 400 Bytes schneller ist als das Nullstellen und Festlegen von 8 Elementen.
Jester
2
Sie haben die Optimierung deaktiviert. Ineffizienter Code ist nicht überraschend, bis Sie überprüfen, ob dasselbe passiert -O3(was auch passiert). godbolt.org/z/rh_TNF
Peter Cordes
12
Was willst du noch wissen? Es ist eine verpasste Optimierung, melden Sie sie auf GCCs Bugzilla mit dem missed-optimizationSchlüsselwort.
Peter Cordes

Antworten:

2

Theoretisch könnte Ihre Initialisierung so aussehen:

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

Daher kann es im Sinne von Cache und Optimierbarkeit effektiver sein, zuerst den gesamten Speicherblock auf Null zu setzen und dann einzelne Werte festzulegen.

Möglicherweise ändert sich das Verhalten in Abhängigkeit von:

  • Zielarchitektur
  • Zielbetriebssystem
  • Array-Länge
  • Initialisierungsverhältnis (explizit initialisierte Werte / Länge)
  • Positionen der initialisierten Werte

In Ihrem Fall wird die Initialisierung natürlich zu Beginn des Arrays komprimiert, und die Optimierung wäre trivial.

Es scheint also, dass gcc hier den allgemeinsten Ansatz verfolgt. Sieht nach einer fehlenden Optimierung aus.

vlad_tepesch
quelle
Ja, eine optimale Strategie für diesen Code wäre wahrscheinlich, alles auf Null zu setzen, oder vielleicht einfach alles, beginnend a[6]mit den frühen Lücken, die mit einzelnen Speichern von Sofort- oder Nullen gefüllt sind. Insbesondere wenn Sie auf x86-64 abzielen, können Sie mithilfe von qword-Speichern zwei Elemente gleichzeitig ausführen, wobei das untere nicht Null ist. zB mov QWORD PTR [rsp+3*4], 1um die Elemente 3 und 4 mit einem falsch ausgerichteten qword-Speicher auszuführen.
Peter Cordes
Das Verhalten könnte theoretisch vom Zielbetriebssystem abhängen, im tatsächlichen GCC jedoch nicht und hat keinen Grund dazu. Nur die Zielarchitektur (und innerhalb dessen wären die Optimierungsoptionen für verschiedene Mikroarchitekturen wie -march=skylakevs. -march=k8vs. -march=knlim Allgemeinen und möglicherweise in Bezug auf die entsprechende Strategie dafür sehr unterschiedlich.)
Peter Cordes
Ist das überhaupt in C ++ erlaubt? Ich dachte, es ist nur C.
Lassie
@Lassie du hast recht in c ++ das ist nicht erlaubt, aber die frage bezieht sich eher auf das compiler backend, so dass es nicht so wichtig ist. auch der angezeigte Code könnte beides sein
vlad_tepesch
Man könnte sogar leicht Beispiele konstruieren , die das gleiche in C ++ arbeiten , indem einige deklarieren struct Bar{ int i; int a[100]; int j;} und initialize Bar a{1,{2,3,4},4};gcc tut das Gleiche: null alle aus, und legen Sie dann die 5 Werte
vlad_tepesch