Vliv kompilačních voleb na rychlost kódu
4.8.2010 00:00 | Radim Kolář | přečteno 5702×
Rozhodl jsem se prozkoumat jaký vliv mají jednotlivé volby kompilátoru na rychlost výsledného kódu obou překladačů.
Jako test jsem použil Linuxový port BYTE benchmarku verze 2. Jde o mixovaný benchmark, kde jsou testovány jak floating point operace tak integer operace a taky je zohledněna práce s pamětí. Jednotlivé testy odpovídají reálným úlohám, nejsou to tedy čistě syntetické benchmarky.
Testování probíhalo pod operačním systémem FreeBSD 8.1 s GCC 4.2.1 20070719 (poslední GCC verze co byla pod GPLv2, novější jsou pod GPLv3). Jako typ CPU pro GCC byl použit Intel prescott, protože core2 architektura v době napsání překladače ještě nebyla. Tento systém běžel virtualizován ve VMware. Přesto doby běhu testů byly konzistentní a lišily se v důsledku virtualizace a zátěží způsobované ostatními aplikacemi až v třetí platné číslici, tedy v jednotkách procent, což víceméně odpovídá přesnosti měření i v nevirtualizovaném prostředí.
V první tabulce se podíváme na GCC. Použijeme 3 sady kompilačních voleb. První a nejpomalejší jsou default kompilační volby používané ve FreeBSD. Druhá sada odpovídá volbám použitým při překladu většiny linux distribucí a třetí sada odpovídá agresivnější -O3 optimalizaci, která se ale moc často v praxi nepoužívá protože naneštěstí GCC často generuje s touto volbou chybný kód nebo kód co je pomalejší než při -O2.
TEST | -O2 -march=prescott -fno-strict-aliasing | -O2 -fstrict-aliasing -fomit-frame-pointer -march=prescott | -march=prescott -O3 -fomit-frame-pointer -funroll-loops |
NUMERIC SORT | 1090 | 1139.3 | 982.44 |
STRING SORT | 95.31 | 96.146 | 96.948 |
BITFIELD | 4.8991e+08 | 4.838e+08 | 4.8311e+08 |
FP EMULATION | 118.45 | 122.67 | 212.62 |
FOURIER | 18845 | 18814 | 19202 |
ASSIGNMENT | 31.617 | 33.296 | 40.036 |
IDEA | 4436 | 4586.3 | 5680.9 |
HUFFMAN | 2116.4 | 2148.8 | 2321.8 |
NEURAL NET | 30.583 | 29.259 | 0.76538 |
LU DECOMPOSITION | 1447.7 | 1465.7 | 1612.5 |
MEMORY INDEX | 15.341 | 15.588 | 16.614 |
INTEGER INDEX | 14.602 | 15.075 | 17.929 |
FLOATING-POINT INDEX | 23.795 | 23.530 | 7.260 |
Zásadní rozdíl mezi první a druhou volbou je volba -fomit-frame-pointer. Touto volbou uvolníme jeden registr za cenu toho, že nám nebude správně fungovat backtrace při debugování. Z crashdump core souborů se pak nedá spolehlivě zjistit v které funkci to spadlo. Registr navíc způsobí zvýšení výkonu protože se nemusí tak často přistupovat do pomalé paměti, ačkoliv na dnešních 64-bitových procesorech již není tak výrazné jako bývalo na 32-bitových Pentium II. Tam uvolnění toho registru znamenalo i 15% nárůst výkonu. Optimalizační volba -O3 přináší oproti -O2 určité, ačkoliv ne výrazné zlepšení. Jak je vidět z výsledků tak -O3 způsobilo u některých testů propad výkonu.
Stejné testy jaké byly provedené s GCC jsou nyní zopakovány s překladačem Clang. Optimalizujeme pro CPU core2, protože ho Clang zná, neboť se jedná oproti starému GCC o současný překladač.
TEST | -O2 -march=core2 | -O2 -fstrict-aliasing -fomit-frame-pointer -march=core2 | -O3 -fstrict-aliasing -fomit-frame-pointer -march=core2 |
NUMERIC SORT | 1320.1 | 1362.9 | 1359.3 |
STRING SORT | 90.122 | 90.62 | 95.615 |
BITFIELD | 4.4253e+08 | 4.4415e+08 | 4.3236e+08 |
FP EMULATION | 217.42 | 224.03 | 223.89 |
FOURIER | 13155 | 13859 | 13933 |
ASSIGNMENT | 30.232 | 31.261 | 30.735 |
IDEA | 4924 | 5128.9 | 5017.2 |
HUFFMAN | 1888 | 1788.4 | 1832.2 |
NEURAL NET | 28.486 | 28.861 | 28.937 |
LU DECOMPOSITION | 1405.5 | 1423.6 | 1392.6 |
MEMORY INDEX | 14.340 | 14.545 | 14.593 |
INTEGER INDEX | 17.786 | 18.002 | 17.997 |
FLOATING-POINT INDEX | 20.411 | 20.949 | 20.852 |
Z tabulky je vidět že Clang generuje pomalejší kód než GCC, ale rozdíl je okolo 10 procent. Clang s -O3 negeneruje oproti -O2 rychlejší kód. Důležitější ale je, že negeneruje v některých případech s -O3 narozdíl od GCC kód pomalejší, takže je volba -O3 u Clang bezpečnější než u GCC. Integer operace umí Clang optimalizovat stejně dobře jako GCC, mírně zaostává při práci s pamětí a ve floating point operacích.
V GCC existuje od verze 4.1 modul ProPolice, který dělá ochranu před buffer overflow útoky. Ochrana není stoprocentní, protože již byly objeveny postupy jak exploitnout i program takto chráněný, ale ne vždy se dá tento postup použít. Pro více informací doporučuji shlédnout prezentaci z projektu OpenBSD.
ProPolice modul byl již dlouho použit v OpenBSD a to dávno v době kdy ještě nebyl oficiální součástí balíku GCC. Druhou distribucí která ho nasadila bylo Ubuntu a pak následovalo DragonFly BSD a NetBSD a FreeBSD-8.
Zajímalo mne jak se tato ochrana projeví na výkonu. ProPolice má dva režimy ochrany v standardním chrání jen fukce volající alloca a funkce s bufferem větším než 8 bajtů. V druhém režimu chrání všechny funkce. Podle materiálů projektu OpenBSD v druhém režimu způsobí ProPolice zhruba 2% zpomalení.
TEST | -fno-stack-protector | -fstack-protector | -fstack-protector-all |
NUMERIC SORT | 1125.9 | 1130.5 | 1063.5 |
STRING SORT | 95.447 | 96.095 | 95.53 |
BITFIELD | 4.8104e+08 | 4.8367e+08 | 4.8313e+08 |
FP EMULATION | 121.45 | 122.38 | 108.15 |
FOURIER | 18670 | 18889 | 18859 |
ASSIGNMENT | 33.208 | 33.375 | 32.583 |
IDEA | 4535.2 | 4536.5 | 4133.7 |
HUFFMAN | 2133.3 | 2243.2 | 2167.5 |
NEURAL NET | 30.011 | 30.354 | 30.796 |
LU DECOMPOSITION | 1443.5 | 1441 | 1436.6 |
MEMORY INDEX | 15.507 | 15.596 | 15.436 |
INTEGER INDEX | 14.924 | 15.158 | 14.021 |
FLOATING-POINT INDEX | 23.549 | 23.716 | 23.794 |
Z tabulky vidíme, že rozdíl mezi volbami -fno-stack-protector a -fstack-protector je pod hranicí chyby měření. U -fstack-protector-all již zpomalení pozorujeme. Zde ale závisí na použitém testu kolik je v něm voláno funkcí. Pokud se funkcí volá hodně, tak pokles může být až 20 procent.
Překladače jazyka C umí optimalizovat program pro daný procesor. Default volba je optimalizovat pro Intel 386. Používat ji není příliš šťastné řešení protože Intel 386 byl vyroben v roce 1986 a dneska se s ním setkáte jen v embedded zařízeních a to ještě velmi velmi zřídka, protože Intely 386 nebyly nikdy oblíbené - místo nich se používala 32-bitová Motorola 68000.
Novější Intel procesory umí nejen nové instrukce, které dokáží nahradit několik starších instrukcí - tedy provedou se rychleji, ale umí zpracovávat i více instrukcí současně za dodržení určitých podmínek - instrukce musí být na sobě nazávislé a požadovaná jednotka procesoru zpracovávající příslušnou operaci musí být volná. Optimalizace kompilátory pro procesor tedy spočívají ve vhodném výběru a řazení instrukcí.
Trochu odlišná situace panuje ve světě RISC procesorů jako jsou UltraSPARC a POWER. Tam se stává že novější verze CPU již neumí z důvodů zjednodušení designu zpracovávat některé málo využívané staré instrukce. Tyto instrukce se pak softwarově emulují, stejně jako se v dobách Intelu 386 a 486SX emuloval matematický koprocesor. Emulace se buďto zabudovaná přímo v operačním systému, nebo se emuluje v userland knihovně, která se při spuštění starých programů načítá mechanizmem LD_PRELOAD.
TEST | GCC i386 | GCC i686 | GCC prescott |
NUMERIC SORT | 1062.2 | 1077.3 | 1139.3 |
STRING SORT | 95.868 | 94.4 | 96.146 |
BITFIELD | 4.8498e+08 | 4.4164e+08 | 4.838e+08 |
FP EMULATION | 71.07 | 122.31 | 122.67 |
FOURIER | 18436 | 18859 | 18814 |
ASSIGNMENT | 32.055 | 33.188 | 33.296 |
IDEA | 4077.8 | 4497.3 | 4586.3 |
HUFFMAN | 2073.7 | 2205.3 | 2148.8 |
NEURAL NET | 28.743 | 31.354 | 29.259 |
LU DECOMPOSITION | 1427.7 | 1438.3 | 1465.7 |
MEMORY INDEX | 15.390 | 15.013 | 15.588 |
INTEGER INDEX | 12.439 | 14.878 | 15.075 |
FLOATING-POINT INDEX | 23.031 | 23.947 | 23.530 |
U GCC platí že pokud vytváříme binární balíčky a nekompilujeme si program pro sebe tak máme kompilovat pro CPU i686 neboli Pentium II CPU. Nárůst výkonu s použitím optimalizace pro vyšší CPU již není tak významný a u zákazníků se mohou čas od času ještě najít starší počítače. Pentium II to sice nebude, ale Celerony kompatibilní s Pentiem III se zřídka zahlédnout dají.
TEST | Clang i386 | Clang i686 | Clang core2 |
NUMERIC SORT | 1365.1 | 1357.2 | 1354.1 |
STRING SORT | 89.648 | 89.722 | 89.786 |
BITFIELD | 4.4283e+08 | 4.4203e+08 | 4.4164e+08 |
FP EMULATION | 223.15 | 222.61 | 215.48 |
FOURIER | 14571 | 14565 | 13620 |
ASSIGNMENT | 30.584 | 30.3 | 31.004 |
IDEA | 5114.9 | 5108.3 | 5074.1 |
HUFFMAN | 1822.5 | 1815.3 | 1779.1 |
NEURAL NET | 10.7 | 10.663 | 28.844 |
LU DECOMPOSITION | 345.92 | 344.41 | 1420.3 |
MEMORY INDEX | 14.373 | 14.324 | 14.433 |
INTEGER INDEX | 18.065 | 18.004 | 17.728 |
FLOATING-POINT INDEX | 9.549 | 9.523 | 20.808 |
U Clangu je situace odlišná. Narozdíl od GCC zde prakticky žádný rozdíl v rychlosti není s vyjímkou floating point operací, které jsou při použití core2 výrazně rychlejší. Pravděpodobně je překladač realizuje pomoci rozšíření SSE.
Překvapil mne všeobecně malý vliv optimalizace větší než -O2 na rychlost výsledného kódu. U Clangu je tento vliv ještě menší. S vyjímkou floating point operací bych řekl že je stěží měřitelný.