¿Por qué la inicialización agregada GCC de una matriz llena todo con ceros primero, incluidos los elementos distintos de cero?

21

¿Por qué gcc llena toda la matriz con ceros en lugar de solo los 96 enteros restantes? Los inicializadores distintos de cero están todos al inicio de la matriz.

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 y gcc9.2 hacen asm como este ( explorador del compilador Godbolt ).

# 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 habilitado, copiaría los 4 inicializadores con movdqa load / store)

¿Por qué GCC no hace lea edi, [esp+16]y memset (con rep stosd) solo los últimos 96 elementos, como lo hace Clang? ¿Es esta una optimización perdida, o es de alguna manera más eficiente hacerlo de esta manera? (Clang realmente llama en memsetlugar de en línea rep stos)


Nota del editor: la pregunta originalmente tenía una salida del compilador no optimizada que funcionaba de la misma manera, pero un código ineficiente -O0no prueba nada. Pero resulta que GCC echa de menos esta optimización incluso en -O3.

Pasar un puntero a auna función no en línea sería otra forma de forzar al compilador a materializarse a[], pero en un código de 32 bits que conduce a un desorden significativo del asm. (Los argumentos de pila dan como resultado empujes, que se mezclan con las tiendas en la pila para iniciar la matriz).

Usar volatile a[100]{1,2,3,4}obtiene GCC para crear y luego copiar la matriz, que es una locura. Normalmente volatilees bueno para ver cómo los compiladores inician las variables locales o las colocan en la pila.

Muchacha
fuente
1
@Damien No entendiste mi pregunta. Pregunto por qué, por ejemplo, a a [0] se le asigna el valor dos veces como si a[0] = 0;y luego a[0] = 1;.
Lassie
1
No puedo leer el ensamblaje, pero ¿dónde muestra que la matriz está completamente llena de ceros?
smac89
3
Otro hecho interesante: para más elementos inicializados, tanto gcc como clang vuelven a copiar toda la matriz desde .rodata... No puedo creer que copiar 400 bytes sea más rápido que poner a cero y configurar 8 elementos.
Bufón
2
Deshabilitó la optimización; el código ineficiente no es sorprendente hasta que verifique que sucede lo mismo en -O3(lo que sucede). godbolt.org/z/rh_TNF
Peter Cordes
12
¿Que más quieres saber? Es una optimización perdida, ve y repórtalo en el bugzilla de GCC con la missed-optimizationpalabra clave.
Peter Cordes el

Respuestas:

2

En teoría, su inicialización podría verse así:

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

por lo tanto, puede ser más efectivo en el sentido de caché y optimizablidad poner primero a cero todo el bloque de memoria y luego establecer valores individuales.

Pueden ser los cambios de comportamiento dependiendo de:

  • arquitectura objetivo
  • sistema operativo de destino
  • longitud de la matriz
  • relación de inicialización (valores / longitud explícitamente inicializados)
  • posiciones de los valores inicializados

Por supuesto, en su caso, la inicialización se compacta al comienzo de la matriz y la optimización sería trivial.

Entonces parece que gcc está haciendo el enfoque más genérico aquí. Parece una optimización que falta.

vlad_tepesch
fuente
Sí, una estrategia óptima para este código probablemente sería poner a cero todo, o tal vez solo todo a partir de a[6]adelante con las brechas iniciales llenas de depósitos únicos de inmediatos o ceros. Especialmente si apunta a x86-64 para que pueda usar qword stores para hacer 2 elementos a la vez, con el inferior distinto de cero. por ejemplo, mov QWORD PTR [rsp+3*4], 1para hacer los elementos 3 y 4 con un almacén de palabras desalineadas
Peter Cordes
El comportamiento podría, en teoría, depender del sistema operativo objetivo, pero en el CCG real no lo hará, y no tiene ninguna razón para hacerlo. Solo la arquitectura de destino (y dentro de eso, las opciones de ajuste para diferentes microarquitecturas, como -march=skylakevs. -march=k8vs. -march=knltodas serían muy diferentes en general, y tal vez en términos de estrategia adecuada para esto)
Peter Cordes
¿Está esto incluso permitido en C ++? Pensé que era solo C.
Lassie
@Lassie tienes razón en c ++, esto no está permitido, pero la pregunta está más relacionada con el backend del compilador, por lo que no importa tanto. También el código mostrado podría ser ambos
vlad_tepesch
Incluso podría construir fácilmente ejemplos que funcionen de la misma manera en C ++ declarando algunos struct Bar{ int i; int a[100]; int j;} e inicializar Bar a{1,{2,3,4},4};gcc hace lo mismo: cero a cero y luego establecer los 5 valores
vlad_tepesch