Mengapa inisialisasi agregat GCC dari array mengisi semuanya dengan nol terlebih dahulu, termasuk elemen yang tidak nol?

21

Mengapa gcc mengisi seluruh array dengan nol alih-alih hanya 96 bilangan bulat yang tersisa? Inisialisasi non-nol semua pada awal 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 dan gcc9.2 keduanya membuat asm seperti ini ( 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

(dengan SSE diaktifkan, ia akan menyalin semua 4 inisialisasi dengan movdqa load / store)

Mengapa GCC tidak melakukan lea edi, [esp+16]dan memset (dengan rep stosd) hanya 96 elemen terakhir, seperti yang dilakukan Clang? Apakah ini optimasi yang tidak terjawab, atau entah bagaimana lebih efisien untuk melakukannya dengan cara ini? (Dentang sebenarnya menelepon memsetbukannya inlining rep stos)


Catatan editor: pertanyaan awalnya adalah keluaran kompiler yang tidak dioptimalkan yang bekerja dengan cara yang sama, tetapi kode yang tidak efisien di -O0tidak membuktikan apa-apa. Tetapi ternyata pengoptimalan ini dilewatkan oleh GCC bahkan pada -O3.

Melewati pointer ke afungsi non-inline akan menjadi cara lain untuk memaksa kompiler terwujud a[], tetapi dalam kode 32-bit yang mengarah pada kekacauan signifikan asm. (Stack args menghasilkan push, yang akan bercampur dengan toko ke stack untuk init array.)

Menggunakan volatile a[100]{1,2,3,4}get GCC untuk membuat dan kemudian menyalin array, yang gila. Biasanya volatilebaik untuk melihat bagaimana kompiler init variabel lokal atau meletakkannya di stack.

Anak dara
sumber
1
@ Damian Anda salah mengerti pertanyaan saya. Saya bertanya mengapa misalnya a [0] diberi nilai dua kali seolah-olah a[0] = 0;dan kemudian a[0] = 1;.
Lassie
1
Saya tidak dapat membaca perakitan, tetapi di mana itu menunjukkan bahwa array diisi seluruhnya dengan nol?
smac89
3
Fakta menarik lainnya: untuk lebih banyak item yang diinisialisasi, baik gcc dan dentang kembali untuk menyalin seluruh array dari .rodata... Saya tidak percaya menyalin 400 byte lebih cepat daripada mem-nolkan dan mengatur 8 item.
Jester
2
Anda menonaktifkan pengoptimalan; kode yang tidak efisien tidak mengherankan sampai Anda memverifikasi bahwa hal yang sama terjadi pada -O3(yang terjadi). godbolt.org/z/rh_TNF
Peter Cordes
12
Apa lagi yang ingin Anda ketahui? Ini optimasi yang terlewat, laporkan di bugzilla GCC dengan missed-optimizationkata kunci.
Peter Cordes

Jawaban:

2

Secara teori inisialisasi Anda dapat terlihat seperti itu:

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

jadi mungkin lebih efektif dalam hal cache dan optimizablity untuk pertama nol seluruh blok memori dan kemudian menetapkan nilai individual.

Mungkin perubahan perilaku tergantung pada:

  • arsitektur target
  • OS target
  • panjang array
  • rasio inisialisasi (nilai / panjang yang diinisialisasi secara eksplisit)
  • posisi nilai yang diinisialisasi

Tentu saja, dalam kasus Anda inisialisasi dipadatkan pada awal array dan optimasi akan sepele.

Jadi sepertinya gcc melakukan pendekatan paling umum di sini. Sepertinya optimasi yang hilang.

vlad_tepesch
sumber
Ya, strategi optimal untuk kode ini mungkin akan nol semuanya, atau mungkin hanya semuanya mulai dari a[6]seterusnya dengan kesenjangan awal diisi dengan satu toko langsung atau nol. Terutama jika menargetkan x86-64 sehingga Anda dapat menggunakan toko qword untuk melakukan 2 elemen sekaligus, dengan yang lebih rendah bukan nol. mis. mov QWORD PTR [rsp+3*4], 1untuk melakukan elemen 3 dan 4 dengan satu toko qword yang tidak selaras.
Peter Cordes
Secara teori perilaku dapat bergantung pada OS target, tetapi dalam GCC aktual tidak akan, dan tidak memiliki alasan untuk itu. Hanya arsitektur target (dan di dalamnya, opsi tuning untuk arsitektur mikro yang berbeda, seperti -march=skylakevs. -march=k8vs. -march=knlakan sangat berbeda secara umum, dan mungkin dalam hal strategi yang tepat untuk ini.)
Peter Cordes
Apakah ini diizinkan di C ++? Saya pikir itu hanya C.
Lassie
@Lassie Anda benar di c ++ ini tidak diperbolehkan, tetapi pertanyaannya lebih terkait dengan backend kompiler, sehingga tidak masalah banyak. juga kode yang ditampilkan dapat berupa keduanya
vlad_tepesch
Anda bahkan dapat dengan mudah membuat contoh yang bekerja sama di C ++ dengan mendeklarasikan beberapa struct Bar{ int i; int a[100]; int j;} dan menginisialisasi Bar a{1,{2,3,4},4};gcc melakukan hal yang sama: nol semua, dan kemudian atur 5 nilai
vlad_tepesch