Optymalizacja kompilacji
1.
Wstęp
Czym są CFLAGS i CXXFLAGS?
CFLAGS i CXXFLAGS są zmiennymi środowiskowymi, które są używane przy kompilacji
kodu przez kompilator gcc. Mówią one jakie przełączniki mają być użyte
podczas kompilacji. Zmienna CFLAGS jest dla kodu napisanego w języku C, zaś
CXXFLAS dla kodu napisanego w C++.
Mogą one przysłużyć się do zmniejszenia ilości informacji diagnostycznych dla
programu, wzrostu poziomu ostrzeżeń o błędach i optymalizacji wygenerowanego
kodu. Podręcznik
GNU gcc zawiera kompletną listę z dostępnymi opcjami i opisem ich
działania.
Jak ich używać?
CFLAGS i CXXFLAGS mogą być użyte na dwa sposoby. Po pierwsze przez program przy
użyciu plików Makefile wygenerowanych przez automake.
Nie należy tego używać podczas instalacji pakietów z drzewa Portage. Od tego są
zmienne CFLAGS i CXXFLAGS w /etc/make.conf. W ten sposób wszystkie
pakiety zostaną skompilowane z danymi opcjami.
Listing 1.1: CFLAGS w /etc/make.conf |
CFLAGS="-march=athlon64 -O2 -pipe"
CXXFLAGS="${CFLAGS}"
|
Jak widać, zmienna CXXFLAGS zawiera wszystkie opcje podane przez CFLAGS. Jest to
bezpieczny sposób, który nigdy nie powinien zawieść. Nigdy nie powinno się
dodawać innych opcji do CXXFLAGS.
Błędne mniemanie
Kiedy mówi się, że CFLAGS i CXXFLAGS mogą być bardzo efektywne, znaczy to, że
produkują mniejszą i/lub szybszą wersję binarną. Ale mogą też osłabić funkcje
kodu oraz powiększyć jego rozmiary, spowolnić czas jego wykonywania a nawet
spowodować niepowodzenie jego kompilacji!
CFLAGS nie jest receptą na sukces. Nie stworzą systemu, który będzie się
uruchamiał szybciej lub też skompilowany kod będzie zajmował mniej miejsca.
Dodawanie większej ilości flag w celu optymalizacji zaowocuje efektem odwrotnym.
Jest to pewna recepta na niepowodzenie.
W internecie można znaleźć informacje o tym, że agresywne flagi mogą wyrządzić
więcej szkód w systemie niż pożytku. Należy pamiętać o tym, że powodem istnienia
flag jest określony cel działania. To, że poszczególne opcje są dobre dla
kawałka kodu, nie oznacza, że są dobre dla wszystkiego, co będzie instalowane w
systemie!
Gotowi?
Skoro już jesteśmy świadomi ryzyka, spójrzmy na kilka bezpiecznych flag dla
naszego systemu. Pozwoli to być pomocnym zawsze, kiedy zostanie zgłoszony
problem na Bugzilli. Deweloperzy zwykle
proszą o ponowną kompilację kodu z minimalnymi flagami, aby dowiedzieć się, czy
problem faktycznie istnieje. Agresywne flagi mogą zrujnować kod.
2.
Optymalizacja
Podstawy
Celem flag jest stworzenie kodu idealnie dopasowanego do konfiguracji
sprzętowej; powinien działać prawidłowo, a zarazem być mały i szybki jak tylko
możliwe. Czasami te dwie cechy się wzajemnie wykluczają, więc trzeba trochę
poeksperymentować z kombinacjami, aby były prawidłowe. Są one dostępne
dla każdej architektury procesora. Agresywne flagi zostaną opisane w
późniejszym etapie. Nie omówimy każdej opcji z podręcznika gcc, ponieważ
są ich setki. Skupimy się na głównych, najczęściej używanych flagach.
Uwaga:
Nie będąc pewnym do czego służy dana flaga, należy odwołać się do odpowiedniej
sekcji Podręcznika
gcc. W razie dalszych wątpliwości można również sprawdzić listę mailingową gcc.
|
-march
Najważniejszą opcją jest -march. Mówi ona kompilatorowi jaki powinien być
kod wynikowy dla używanej architektury
(arch) procesora. Różne rodzaje procesorów mają różne zdolności,
wspierają różne instrukcje i mają różne rodzaje wykonywania kodu. Flaga
-march ma za zadanie stworzenie kodu, który będzie specyficzny dla danego
procesora, ze wszystkimi tymi zdolnościami, cechami, instrukcjami itp.
Nawet jeśli zmienna CHOST jest ustawiona w pliku /etc/make.conf i
określa używaną architekturę, flaga -march powinna być użyta aby programy
mogły zostać zoptymalizowane dla konkretnego procesora. Architektury x86 i
x86-64 powinny używać flagi -march.
Aby dowiedzieć się, jaki rodzaj procesora jest w komputerze, którego używamy,
należy wykonać polecenie:
Listing 2.1: Analiza informacji o procesorze |
$ cat /proc/cpuinfo
|
Oglądnijmy -march w akcji. Przykład jest dla starszego procesora Pentium
III
Listing 2.2: /etc/make.conf: Pentium III |
CFLAGS="-march=pentium3"
CXXFLAGS="${CFLAGS}"
|
Ten przykład pochodzi z 64-bitowego procesora AMD.
Listing 2.3: /etc/make.conf: AMD64 |
CFLAGS="-march=athlon64"
CXXFLAGS="${CFLAGS}"
|
Dostępne są również flagi -mtune oraz -mcpu. Jednak powinny zostać
one użyte tylko wtedy, gdy opcja -march nie jest dostępna. Pewne
architektury procesorów mogą wymagać -mtune lub nawet -mcpu.
Niestety zachowanie gcc z użyciem tych flag nie jest identyczne na
wszystkich architekturach.
Na procesorach x86 i x86-64, -march tworzy kod specyficzny dla tych
architektur, używając wszystkich możliwych instrukcji i odpowiedniego ABI. Nie
będzie kompatybilne dla starszych procesorów. Jeśli nie zamierzamy uruchamiać
kodu na niczym innym jak nasz system, powinniśmy używać -march. Użycie
-mtune można rozważyć w przypadku kompilacji dla starszego typu procesora
takiego jak i386 lub i486. -mtune tworzy bardziej ogólny kod niż
-march lecz w dalszym ciągu odpowiedni dla pewnego procesora. Nie należy
używać -mcpu na systemach x86 i x86-64.
Inne architektury niż x86/x86-64 (takie jak Sparc, Alpha lub PowerPC) mogą
wymagać użycia -mtune lub -mcpu zamiast -march. Na tych
architekturach -mtune/-mcpu może czasami zachowywać się jak
-march (na x86/x86-64) jedynie podając inną nazwę. Jeszcze raz:
zachowanie gcc nie jest identyczne na wszystkich architekturach, więc
należy sprawdzić stronę podręcznika
gcc aby dowiedzieć się którą opcję użyć dla posiadanego procesora.
Uwaga:
Dla konkretniejszych ustawień -march/-mtune/-mcpu należy
przeczytać rozdział piąty Podręcznika instalacji Gentoo dla
odpowiedniej architektury. Polecamy również przeczytać listę podręcznika
gcc o opcjach
specyficznych dla danej architektury gdzie jest dokładnie wytłumaczona
różnica pomiędzy -march, -mcpu i -mtune.
|
-O
Następną opcją jest -O. Kontroluje ona całkowity poziom optymalizacji.
Może ona spowodować, że kod będzie się kompilował nieco dłużej i zostanie użyte
nieco więcej pamięci wraz z jej podnoszeniem.
Jest pięć opcji -O: -O0, -O1, -O2, -O3 oraz
-Os. W /etc/make.conf powinna być użyta tylko jedna.
Z wyjątkiem -O0, każda następna opcja -O aktywuje kilka
dodatkowych flag. O tym jakie flagi są aktywowane przy każdym poziomie -O
i co powodują, można się dowiedzieć po przeczytaniu rozdziału o opcjach
optymalizacji strony manuala gcc.
Przeanalizujmy każdy poziom optymalizacji:
-
-O0: Ten poziom (jest to litera "O" za którą jest cyfra zero)
kompletnie wyłącza optymalizację i jest domyślna kiedy opcja -O nie
jest określona w zmiennych CFLAGS i CXXFLAGS, co nie jest generalnie
pożądane.
-
-O1: Bardzo podstawowy poziom optymalizacji. Kompilator będzie
próbował budować szybszy i mniejszy kod bez tracenia na to czasu. Jest to
naprawdę podstawowy poziom i nie powinien stanowić problemów podczas
kompilacji.
-
-O2: Poziom wyżej od -O1. Jest to zalecany poziom
optymalizacji jeżeli nie mamy specjalnych potrzeb. -O2 do -O1
doda kilka nowych flag. Z opcją -O2 kompilator będzie próbował
zwiększyć wydajność kodu nie zwracając uwagi na rozmiar kodu wynikowego przy
długim czasie kompilacji.
-
-O3: Najwyższy możliwy poziom optymalizacji i zarazem najbardziej
ryzykowny. Z tą opcją kompilator będzie tracił dużo czasu na stworzenie kodu
wynikowego. Generalnie opcja nie powinna być używana na systemach
używających gcc w wersji 4.x. Zachowanie gcc znacznie się
zmieniło od wersji 3.x, gdzie opcja -O3 miała prowadzić do trochę
szybszego wykonania niż przy -O2. Przy wersji kompilatora gcc
4.x, kompilowany z użyciem -O3 kod będzie miał wielki rozmiar,
a jego wykonanie będzie wymagało użycia większej ilości pamięci. Będzie
również znacznie zwiększał przypadki niepowodzenia kompilacji i
nieoczekiwane zachowanie programów. Używanie -O3 nie jest zalecane
dla gcc 4.x.
-
-Os: Ten poziom będzie optymalizował kod pod względem wielkości.
Aktywuje on wszystkie flagi z poziomu -O2, które nie biorą udziału w
zwiększeniu wygenerowanego kodu. Może to być przydatne dla maszyn, które
mają bardzo ograniczoną przestrzeń dyskową lub z procesorami o bardzo małej
wielkości cache. Może jednak spowodować sporo problemów, ponieważ filtrowane
przez wiele ebuildów z drzewa Portage. Używanie -Os nie jest
zalecane.
Jak zostało wcześniej wspomniane, -O2 jest rekomendowanym poziomem
optymalizacji. Jeśli pakiet zawiedzie podczas kompilacji, należy się upewnić, że
nie jest używana opcja -O3. Przed zgłoseniem błędu należy spróbować
zbudować pakiet z użyciem niższego poziomu optymalizacji takim jak -O1
lub nawet -O0 -g2 -ggdb.
-pipe
-pipe jest bezpieczną flagą. Nie ma ona wpływu na generowany kod, lecz
przynosi zysk w prędkości kompilacji. Każe kompilatorowi używać potoków zamiast
plików tymczasowych w różnych etapach kompilacji.
-fomit-frame-pointer
Jest to bardzo powszechna flaga stworzona do zmniejszania wielkości generowanego
kodu. Jest ona włączona w każdym poziomie -O (poza -O0) na
architekturach gdzie nie przeszkadza to przy debugowaniu (takich jak np.
x86-64). Ponieważ strona podręcznika GNU gcc nie podaje wszystkich
architektur, na których -O wymusza działanie tej flagi, czasem trzeba ją
dodać własnoręcznie. Używanie tej flagi może powodować problemy podczas
debugowania kodu.
Szczególne problemy sprawiają aplikacje napisane w Javie, chociaż nie jest ona
jedynym językiem napiętnowanym przez tę flagę. Trzeba mieć na uwadze, że mimo
tego, że flaga ogólnie pomaga, powoduje trudności podczas debugowania kodu;
backtrace będzie bezużyteczne. Jeśli nie zamierzamy poddawać kodu operacjom
debugowania i nie mamy w zmiennej CFLAGS innych flag, takich jak -ggdb,
warto dodać -fomit-frame-pointer.
Ważne:
Nie należy łączyć -fomit-frame-pointer z podobnie działającą flagą
-momit-leaf-frame-pointer. Użycie drugiej flagi jest niepotrzebne,
ponieważ -fomit-frame-pointer wykonuje swoją pracę prawidłowo. Ponadto,
-momit-leaf-frame-pointer okazuje się negatywnie wpływać na jakość kodu.
|
-msse, -msse2, -msse3, -mmmx, -m3dnow
Te flagi uruchamiają ustawienia instrukcji
SSE,
SSE2, SSE3, MMX i 3DNow! dla architektur x86 i
x86-64. Jest to użyteczne przede wszystkim przy multimediach, grach i zadaniach,
wymagających intensywnych obliczeń matematycznych. Funkcje te są dostępne w
nowszych modelach procesorów.
Ważne:
Należy się upewnić, że dany procesor wspiera powyższe flagi. Można się tego
dowiedzieć poprzez cat /proc/cpuinfo. Wyjście tego polecenia będzie
zawierało wszystkie dodatkowe ustawienia. Trzeba mieć na uwadze, że pni
jest inną nazwą dla SSE3.
|
Generalnie nie ma potrzeby dodawania jakiejkolwiek z tych flag, dopóki jest
ustawiona odpowiednia opcja -march (np. -march=nocona zawiera
-msse3). Kilka wartych uwagi wyjątków to nowsze procesory VIA i AMD64,
które wspierają instrukcje niezawarte w -march (takie jak SSE3). Dla
takich procesorów będzie wymagane dodanie flag, które będą odpowiadały wynikowi
cat /proc/cpuinfo.
Uwaga:
Należy zapoznać się z listą
flag specyficznych tylko dla architektur x86 i x86-64, aby wiedzieć co aktywuje
każda z tych ustawień. Gdy instrukcja jest podana, wtedy nie ma potrzeby jej
wyszczególnienia. Będzie ona domyślnie włączona przy odpowiedniej fladze
-march.
|
3.
FAQ Optymalizacji
Ale można osiągnąć lepsze wyniki przy użyciu -funroll-loops -fomg-optimize!
Niestety. Może się tylko wydawać, ponieważ ktoś nas przekonał, że
wydajność rośnie z liczbą użytych flag. Agresywne flagi mogą tylko uszkodzić
aplikacje. Nawet strona manuala
gcc mówi, że użycie -funroll-loops i -funroll-all-loops
tworzy kod większy i bardziej powolny. Po -ffast-math,
-fforce-mem, -fforce-addr, są to kolejne mylnie używane flagi.
Prawdą jest, że są to bardzo agresywne flagi. Można się o tym przekonać w bardzo
łatwy sposób: odwiedzając Forum
Gentoo oraz Bugzillę. Szybko
zauważymy że nie robią one nic dobrego!
Nie należy ich używać, ponieważ stworzą one spuchnięty kod o wielkich rozmiarach
i słabej wydajności. Sprawią, że system będzie działał na krawędzi, a nasze bugi
zostaną oznaczone jako INVALID lub WONTFIX.
Nie potrzeba tak niebezpiecznych flag. Nie należy ich używać. Lepsze od
tego jest użycie podstawowych flag: -march, -O i -pipe.
Co z poziomami -O wyższymi niż 3?
Niektórzy użytkownicy chwalą się lepszą wydajnością przy użyciu -O4,
-O9 itp. Prawdą jednak jest to, że poziomy wyższe niż 3 nie mają żadnego
wpływu. Kompilator akceptuje CFLAGS takie jak -O4, lecz nie wykorzystuje
ich przy budowie kodu. Jest to równoznaczne z użyciem -O3, nic więcej.
Więcej dowodów? Przeanalizuj kod
źródłowy gcc:
Listing 3.1: Kod źródłowy -O |
if (optimize >= 3)
{
flag_inline_functions = 1;
flag_unswitch_loops = 1;
flag_gcse_after_reload = 1;
/* Allow even more virtual operators. */
set_param_value ("max-aliased-vops", 1000);
set_param_value ("avg-aliased-vops", 3);
}
|
Jak widać, każda wartość większa niż 3 jest traktowana jak -O3.
Co ze zbędnymi flagami?
Opcje CFLAGS i CXXFLAGS, które są zawarte w różnych poziomach -O są
zbędne w /etc/make.conf. Czasami są one ignorowane, a czasami
filtrowane lub zastępowane.
Filtracje i zastępowanie flag jest wykorzystywane przez wiele ebuildów w drzewie
Portage. Zazwyczaj jest to robione z powodu niepowodzenia przy kompilacji z
określonym poziomem optymalizacji -O lub kiedy kod jest zbyt wrażliwy na
wszelkie dodatkowe flagi. Ebuild będzie filtrował niektóre CFLAGS i CXXFLAGS lub
zastąpi -O innym poziomem.
Podręcznik
Dewelopera Gentoo tworzy szkic gdzie i jak filtrowanie i zastępowanie flag
działa.
Można ominąć filtrowania -O poprzez niepotrzebne dodawanie
flag pewnych poziomów optymalizacji, takich jak -O3
Listing 3.2: Zbędne opcje w CFLAGS |
CFLAGS="-O3 -finline-functions -funswitch-loops"
|
Nie jest to mądre. CFLAGS jest filtrowane z konkretnego powodu!
Kiedy flagi są filtrowane, znaczy to, że są niebezpieczne do budowy pakietu.
Naprawdę, nie jest bezpieczne budowanie całego systemu z -O3 jeśli
jakaś flaga z tego poziomu optymalizacji będzie tworzyła problemy z danymi
pakietami. Zaufaj deweloperom. Filtrowanie i zastępowanie flag jest
robione dla bezpieczeństwa naszych danych. Jeśli ebuild określa alternatywną
flagę, nie należy tego obchodzić w żaden sposób!
Po zbudowaniu pakietu z nieakceptowanymi flagami, najczęściej pogłębiamy się w
problemach. Kiedy zgłaszamy błąd na Bugzilli, użyte flagi z
/etc/make.conf są widoczne. Zostaniemy wtedy powiadomieni, aby
przebudować dany pakiet bez niepotrzebnych flag. Można sobie tego oszczędzić
poprzez ich nieużywanie i niezakładanie, że wie się lepiej niż deweloperzy.
Co z LDFLAGS?
Deweloperzy Gentoo ustawili w profilu podstawowe i bezpieczne wartości LDFLAGS
dlatego nie ma potrzeby ich definiowania.
Czy można używać flag dla pojedyńczych pakietów?
W tej chwili nie ma żadnej metody wspieranej przez nas, aby używać CFLAGS lub
innych zmiennych dla jednego pakietu. Istnieje jednak kilka metod na
zmuszenie Portage do takiego działania.
Nie powinniśmy jednak zmuszać Portage do używania flag dla jednego
pakietu, ponieważ takie działanie nie jest w żaden sposób wspierane i
najprawdopodobniej skomplikuje wysyłanie ewentualnych raportów dotyczących
błędów. Należy ustawić odpowiednie zmienne w pliku /etc/make.conf
tak, aby były one używane w całym systemie.
4.
Zasoby
Poniższe źródła są pomocne dla dalszego zrozumienia optymalizacji:
Materiał udostępniany na podstawie licencji Creative Commons -
Attribution / Share Alike.
|