Perché l'inizializzazione aggregata GCC di un array riempie per prima l'intera cosa di zeri, inclusi elementi diversi da zero?


21

Perché gcc riempie l'intero array di zeri invece che solo dei restanti 96 numeri interi? Gli inizializzatori diversi da zero sono tutti all'inizio dell'array.

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 e gcc9.2 fanno entrambi asm in questo modo ( 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

(con SSE abilitato avrebbe copiato tutti e 4 gli inizializzatori con carico / archivio movdqa)

Perché GCC non fa lea edi, [esp+16]e memorizza (con rep stosd) solo gli ultimi 96 elementi, come fa Clang? È un'ottimizzazione mancata o è in qualche modo più efficiente farlo in questo modo? (Clang in realtà chiama memsetinvece di inline rep stos)


Nota del redattore: la domanda originariamente aveva un output del compilatore non ottimizzato che funzionava allo stesso modo, ma il codice inefficiente su -O0non dimostra nulla. Ma si scopre che questa ottimizzazione manca anche a GCC -O3.

Passare un puntatore a auna funzione non inline sarebbe un altro modo per forzare il compilatore a materializzarsi a[], ma nel codice a 32 bit che porta a un ingombro significativo dell'asm. (Stack args si traduce in push, che viene mischiato con i negozi nello stack per avviare l'array.)

Utilizzando si volatile a[100]{1,2,3,4}ottiene GCC per creare e quindi copiare l'array, il che è folle. Normalmente volatileè utile osservare come i compilatori inizializzano le variabili locali o le dispongono nello stack.


1
@Damien Hai frainteso la mia domanda. Chiedo, ad esempio, perché a [0] sia assegnato il valore due volte come se a[0] = 0;e poi a[0] = 1;.
Lassie,

1
Non riesco a leggere l'assembly, ma dove mostra che l'array è pieno di zeri?
smac89,

3
Un altro fatto interessante: per più elementi inizializzati, sia gcc che clang tornano a copiare l'intero array da .rodata... Non posso credere che copiare 400 byte sia più veloce dell'azzeramento e impostazione di 8 elementi.
Giullare

2
Hai disabilitato l'ottimizzazione; codice inefficiente non è sorprendente fino a quando non si verifica che la stessa cosa accada -O3(cosa che fa). godbolt.org/z/rh_TNF
Peter Cordes,

12
Cos'altro vuoi sapere? È un'ottimizzazione mancata, vai a segnalarlo sulla bugzilla di GCC con la missed-optimizationparola chiave.
Peter Cordes,

Risposte:


2

In teoria la tua inizializzazione potrebbe apparire così:

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

quindi potrebbe essere più efficace in termini di cache e ottimizzazione prima di azzerare l'intero blocco di memoria e quindi impostare i singoli valori.

Potrebbe essere il comportamento cambia a seconda di:

  • architettura target
  • sistema operativo di destinazione
  • lunghezza della matrice
  • rapporto di inizializzazione (valori / lunghezza esplicitamente inizializzati)
  • posizioni dei valori inizializzati

Naturalmente, nel tuo caso l'inizializzazione viene compattata all'inizio dell'array e l'ottimizzazione sarebbe banale.

Quindi sembra che gcc stia facendo l'approccio più generico qui. Sembra un'ottimizzazione mancante.


Sì, una strategia ottimale per questo codice sarebbe probabilmente quella di azzerare tutto, o forse solo tutto a partire da in a[6]poi con i primi vuoti riempiti con singoli negozi di immediati o zeri. Soprattutto se hai come target x86-64 in modo da poter usare qword store per fare 2 elementi contemporaneamente, con quello inferiore diverso da zero. ad es. mov QWORD PTR [rsp+3*4], 1per eseguire gli elementi 3 e 4 con un archivio qword disallineato.
Peter Cordes, il

In teoria, il comportamento potrebbe dipendere dal sistema operativo di destinazione, ma in realtà GCC non lo farà e non ha motivo di farlo. Solo l'architettura di destinazione (e al suo interno, le opzioni di ottimizzazione per diverse microarchitetture, come -march=skylakevs. -march=k8vs. -march=knlsarebbero tutte molto diverse in generale, e forse in termini di strategia appropriata per questo.)
Peter Cordes

È consentito anche in C ++? Ho pensato che fosse solo C.
Lassie il

@Lassie hai ragione in c ++ questo non è permesso, ma la domanda è più correlata al backend del compilatore, quindi non ha molta importanza. anche il codice mostrato potrebbe essere entrambi
vlad_tepesch il

Potresti anche facilmente costruire esempi che funzionano allo stesso modo in C ++ dichiarandone alcuni struct Bar{ int i; int a[100]; int j;} e inizializzando Bar a{1,{2,3,4},4};gcc fa la stessa cosa: azzera tutto e poi imposta i 5 valori
vlad_tepesch
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.