Wypadałoby na wstępie nadmienić o ogromnych możliwościach tegoż kompilatora, ale mi się nie chce, skoro wszystko to można poznać na stronach Wikipedii. W każdym bądź razie nie można zaprzeczyć, że generuje niczego sobie kod, szczególnie jeśli mowa o optymalizacjach. No i wszystko to jest za darmo. Wspomniane optymalizacje zaczęły mnie interesować przy pisaniu kodu pod mikroprocek AVR. Kompilowałem sobie kod, z różnymi opcjami optymalizacyjnymi i sprawdzałem przy użyciu IDA'y (jeszcze będzie tu użyty ten disassembler) jakiż to kod asemblera został wygenerowany... Okazywało się, że często wyprawiał niesamowite rzeczy by przyspieszyć kod.
W ten oto sposób chcę przejść do tematu tego wpisu. Od jakiegoś czasu tworzony jest w gcc kod pozwalający w ramach optymalizacji dokonać autowektoryzacji pętli (jeśli są tam jakieś operacje na tablicach). Wersja gcc na moim kompie to 4.1.2 (mam też 4.2 ale z racji, że ta pierwsza wersja jest bardziej rozpowszechniona to na niej się skupie) i posiada on już możliwość użycia tego, dając zaskakujące rezultaty - dodać też trzeba, że optymalizacja ta jest wciąż rozwijana i pełne możliwości będzie można obserwować dopiero w przyszłości. Najlepiej zobrazować to jednak przy użyciu prostego przykładu.
Mam ci tu ja banalny kawałek kodu, który zostanie później skompilowany z i bez odpowiednich opcji optymalizacyjnych:
1 #include <iostream>
2
3 const int N = 65536;
4
5 int main()
6 {
7 float a[N], b[N], c[N];
8 float d[N], e[N];
9
10 for(int j=0; j < 1024; j++)
11 {
12 for(int i=0; i<N; i++)
13 {
14 a[i] = b[i] * c[i] + 3;
15 e[i] = (d[i] / 5.0f + a[i]) / 2.0f;
16 }
17 }
18
19 std::cout << a[17] * e[50] << std::endl;
20
21 return 0;
22 }
Ten cout jest konieczny, ponieważ bez niego optymlizator stwierdza, że przetwarzane dane nie są nigdzie wykorzystane, więc po co się męczyć i pętla w kodzie nie istnieje. Standardowa kompilacja wykonywana jest z następującymi parametrami:
g++ test_vec.cpp -o test_vec -O2 -march=pentium-m -msse2
, a ta włączająca automatyczną wektoryzację to lekki rozszerzenie powyższego:
g++ test_vec.cpp -o test_vec -O2 -march=pentium-m -msse2 -ftree-vectorize -ftree-vectorizer-verbose=6
Parę słów na temat doboru powyższych parametrów. Oba wywołania różnią się tylko dwoma ostatnimi parametrami (patrząc na drugie wywołanie) - przedostatni włącza omawianą optymalizację (a właściwie jak dalej wyjdzie próbę jej zastosowania), a ostatni odpowiada za wyrzucanie informacji o tym, która pętla została (lub nie) zoptymalizowana i dlaczego (jeśli do optymalizacji nie doszło). 6 to ostania sensowna wartość (wg. mnie), ponieważ przy większej kompilator zesypie nas dziesiątkami linii tekstu na temat jego przemyśleń z serii "to optimize, or not to optimize". Konieczne jest też dodanie parametru włączającego optymalizację (-Ox), bo tak. Wektoryzacja wykorzystuje zestaw operacji sse/sse2 więc także o tym należy poinformować (-msse lub -msse2). Parametr odpowiadający za generację kodu pod mój procek to już tylko moja zachcianka.
Podczas kompilacji drugim "zestawem" gcc wypluje coś na kształt poniższego:
test_vec.cpp:10: note: not vectorized: nested loop.
test_vec.cpp:12: note: LOOP VECTORIZED.
test_vec.cpp:5: note: vectorized 1 loops in function.
To właśnie wspomniana informacja, o wewnętrznych rozterkach kompilatora na temat uszczęśliwienia nas szybciej działającym kodem.
Czymże by jednak było omawianie nowej optymalizacji (jeśli się mylę co, do słowa nowej to proszę o poprawę... i nie chodzi mi tu kompilator intela, który ma to podobno lepiej zrobione) bez informacji o możliwym zysku czasowym. Czas więc na mały benchmark. Wykonywany jest pod Linuksem najłatwiejszym z możliwych sposobów... czyli nieśmiertelnym:
time ./test_vec
wielokrotnie wykonywanym w celu wyciągnięcia jakiejś reprezentacyjnej średniej, tudzież innej dominanty :P. Oczywistym jest, że na innych prockach (mój laptop wyposażony jest w Celeron M) mogą się znacznie różnić. Najczęstszy wynik dla kodu bez autowektoryzacji to:
real 0m1.542s
user 0m1.428s
sys 0m0.008s
Włączając opisywaną optymalizację wyniki oscylują wokoło poniższego:
real 0m0.436s
user 0m0.420s
sys 0m0.000s
Nie ma nawet czego komentować. A dla gcc w wersji 4.2.1 jest jeszcze lepiej...
No ale, co wpłynęło na taki przyrost prędkości. Tu wkracza na nasz wesoły poligon disassember IDA. Jak dla mnie dzieło programistycznej sztuki. Wystarczy potraktować tym kod i już zaglądamy w jego ukryte sekrety (ech.. czuje się jak reporter Faktów)... czyli coś dla lubiących czytać kod maszynowy.
Na poniższych obrazkach widać kod przed

i po

optymalizacji. Pokazałem tam tylko interesujący nas kawałek, związany z wykonaniem pętli, plus mały komentarzyk dla czujących odrazę do kodu maszynowego. Na pewno widać, że użyłem w kodzie okrągłych wartości (znaczy potęg 2) ale nie jest to ważne, bo pętle te zostaną zoptymalizowane także dla nie tak ładnych liczb. Dla potęg 2 kod ten (w asemblerze) jest po prostu bardziej czytelny.
Nie ma jednak róży bez ognia... tfu kolców... Mianowicie żeby kompilator optymalizował tak ładnie nasze pętle, konieczne jest świadome ich konstruowanie. Ponieważ kod tego optymalizatora jest jeszcze ciągle rozwijany, to nie potrafi optymalizować niektórych konstrukcji. Przykłady rozpoznawanych peŧli podane są na tej stronie - polecam tam spojrzeć, by się dowiedzieć się gdzie można liczyć na przyspieszenie.
Byłbym zapomniał - za jakiś czas optymalizacja ta zostanie włączona pod -O3. Nic, tylko cierpliwie czekać.