Dlaczego gcc wypełnia całą tablicę zerami zamiast tylko pozostałych 96 liczb całkowitych? Wszystkie niezerowe inicjalizatory znajdują się na początku tablicy.
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
}
Zarówno MinGW8.1, jak i gcc9.2 tworzą asm ( eksplorator kompilatora 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
(z włączonym SSE kopiowałby wszystkie 4 inicjatory z movdqa load / store)
Dlaczego GCC nie robi lea edi, [esp+16]
i nie zapisuje (z rep stosd
) tylko ostatnich 96 elementów, tak jak Clang? Czy jest to pominięta optymalizacja, czy może jest to w jakiś sposób bardziej wydajne? (Clang faktycznie dzwoni memset
zamiast wstawiania rep stos
)
Uwaga edytora: pytanie pierwotnie zawierało niezoptymalizowane wyjście kompilatora, które działało w ten sam sposób, ale nieefektywny kod w -O0
nic nie dowodzi. Okazuje się jednak, że GCC nie dostrzega tej optymalizacji nawet przy -O3
.
Przekazywanie wskaźnika do a
funkcji innej niż wbudowana byłoby innym sposobem zmuszenia kompilatora do zmaterializowania się a[]
, ale w 32-bitowym kodzie, który prowadzi do znacznego zaśmiecenia asm. (Argumenty stosu powodują wypychanie, które zostaje wmieszane ze sklepami do stosu w celu zainicjowania tablicy).
Użycie volatile a[100]{1,2,3,4}
powoduje, że GCC tworzy, a następnie kopiuje tablicę, co jest szalone. Zwykle volatile
dobrze jest sprawdzić, jak kompilatory inicjują zmienne lokalne lub układają je na stosie.
a[0] = 0;
i wtedya[0] = 1;
..rodata
... Nie mogę uwierzyć, że skopiowanie 400 bajtów jest szybsze niż zerowanie i ustawienie 8 elementów.-O3
(co robi). godbolt.org/z/rh_TNFmissed-optimization
słowa kluczowego.Odpowiedzi:
Teoretycznie inicjalizacja może wyglądać tak:
więc może być bardziej efektywne w sensie pamięci podręcznej i optymalizacji, aby najpierw wyzerować cały blok pamięci, a następnie ustawić poszczególne wartości.
Mogą być zmiany zachowania w zależności od:
Oczywiście w twoim przypadku inicjalizacja jest kompaktowana na początku tablicy, a optymalizacja byłaby trywialna.
Wygląda więc na to, że gcc stosuje tutaj najbardziej ogólne podejście. Wygląda na brakującą optymalizację.
źródło
a[6]
początku, z wczesnymi lukami wypełnionymi pojedynczymi zbiorami bezpośrednich lub zer. Zwłaszcza, jeśli celujesz w x86-64, abyś mógł używać sklepów qword do zrobienia 2 elementów jednocześnie, przy czym dolny jest niezerowy. np.mov QWORD PTR [rsp+3*4], 1
zrobić elementy 3 i 4 z jednym źle dopasowanym magazynem qword.-march=skylake
vs.-march=k8
vs.-march=knl
byłyby ogólnie bardzo różne, i być może pod względem odpowiedniej strategii do tego.)struct Bar{ int i; int a[100]; int j;}
i inicjującBar a{1,{2,3,4},4};
gcc, robi to samo: