Por que o GCC agregou a inicialização de uma matriz primeiro preenchendo tudo com zeros, incluindo elementos diferentes de zero?

21

Por que o gcc preenche toda a matriz com zeros em vez de apenas os 96 números inteiros restantes? Os inicializadores diferentes de zero estão todos no início da 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
}

O MinGW8.1 e o gcc9.2 fazem o asm assim ( 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

(com o SSE ativado, ele copiaria todos os 4 inicializadores com o movdqa load / store)

Por que o GCC não faz lea edi, [esp+16]e memset (com rep stosd) apenas os últimos 96 elementos, como Clang faz? É uma otimização perdida ou é, de alguma forma, mais eficiente fazê-lo dessa maneira? (Clang realmente chama em memsetvez de inlining rep stos)


Nota do editor: a pergunta originalmente tinha saída de compilador não otimizada que funcionava da mesma maneira, mas código ineficiente em -O0não prova nada. Mas acontece que essa otimização é perdida pelo GCC mesmo em -O3.

Passar um ponteiro para auma função não embutida seria outra maneira de forçar o compilador a se materializar a[], mas no código de 32 bits que leva a uma confusão significativa do asm. (Args da pilha resultam em pushes, que são misturados com os armazenamentos na pilha para iniciar a matriz.)

Usar volatile a[100]{1,2,3,4}faz com que o GCC crie e copie a matriz, o que é insano. Normalmente volatileé bom ver como os compiladores iniciam variáveis ​​locais ou as colocam na pilha.

Lassie
fonte
11
@ Damien Você entendeu mal a minha pergunta. Eu pergunto por que, por exemplo, o valor a [0] é atribuído duas vezes como se a[0] = 0;e depois a[0] = 1;.
Lassie
11
Não consigo ler a montagem, mas onde isso mostra que a matriz é preenchida inteiramente com zeros?
Smac89
3
Outro fato interessante: para mais itens inicializados, tanto o gcc quanto o clang voltam a copiar toda a matriz de .rodata... Não acredito que copiar 400 bytes seja mais rápido do que zerar e definir 8 itens.
Jester
2
Você desativou a otimização; código ineficiente não é surpreendente até que você verifique se o mesmo acontece em -O3(o que acontece). godbolt.org/z/rh_TNF
Peter Cordes
12
O que mais você quer saber? É uma otimização perdida, denuncie no bugzilla do GCC com a missed-optimizationpalavra - chave.
Peter Cordes

Respostas:

2

Em teoria, sua inicialização poderia ser assim:

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

portanto, pode ser mais eficaz no sentido de cache e otimização primeiro zerar todo o bloco de memória e depois definir valores individuais.

Pode ser que o comportamento mude dependendo de:

  • arquitetura de destino
  • SO alvo
  • comprimento da matriz
  • taxa de inicialização (valores explicitamente inicializados / comprimento)
  • posições dos valores inicializados

Obviamente, no seu caso, a inicialização é compactada no início da matriz e a otimização seria trivial.

Portanto, parece que o gcc está fazendo a abordagem mais genérica aqui. Parece uma otimização ausente.

vlad_tepesch
fonte
Sim, uma estratégia ideal para esse código provavelmente seria zerar tudo, ou talvez apenas tudo, começando do início a[6]com as lacunas iniciais preenchidas com reservas únicas de imediatos ou zeros. Especialmente se estiver segmentando x86-64, para que você possa usar os repositórios qword para criar 2 elementos de uma só vez, com o menor menor que zero. por exemplo, mov QWORD PTR [rsp+3*4], 1para executar os elementos 3 e 4 com um armazenamento qword desalinhado.
Peter Cordes
O comportamento poderia, em teoria, depender do sistema operacional de destino, mas no GCC real não será e não há motivo para isso. Somente a arquitetura de destino (e, dentro disso, as opções de ajuste para diferentes microarquiteturas, como -march=skylakevs. -march=k8vs., -march=knlseriam todas muito diferentes em geral, e talvez em termos de estratégia apropriada para isso.)
Peter Cordes
Isso é permitido em C ++? Eu pensei que é apenas C.
Lassie
@Lassie, você está certo em c ++, isso não é permitido, mas a questão está mais relacionada ao back-end do compilador, de modo que isso não importa muito. também o código mostrado pode ser ambos
vlad_tepesch 23/01
Você pode facilmente construir exemplos que funcionam da mesma maneira em C ++, declarando alguns struct Bar{ int i; int a[100]; int j;} e inicializando o Bar a{1,{2,3,4},4};gcc faz a mesma coisa: zere tudo e defina os 5 valores
vlad_tepesch