Nejdůležitější GNU program je nesporně překladač. Ten umožnil, aby vzniklo všechno ostatní. Jedná se o jeden z nejkvalitnějších překladačů vůbec - je snadno přenositelný na novou architekturu (jedná se pouze o napsaní několika souborů s popisem architektury a pro generování assembleru), umí překládat mnoho různých jazyků (není tedy třeba jeden překladač pro Pascal a jiný pro C) a má výborný optimizer.
Klasický překladač napřed načte vstup, rozloží ho na tokeny a porozumí mu. Když už ví, co programátor napsal, přidělí paměťová místa proměnným (modernější používají i registry) a potom pomocí jakýchsi prefabrikátů na každý příkaz jazyka složí výsledný kód. Případně se ještě pokusí předvyhodnotit výrazy, nebo se rozhlédne po vzniklém assembleru a vymění pár instrukcí, které se často špatně generují. Vzniklou skládačku potom pošle dál.
Překladač od GNU má jiné řešení. Nejprve porozumí programu. Toto je jediná část závislá na jazyce a nazývá se parsing. Potom se vygeneruje mezijazyk zvaný Register transfer language (RTL). Od tohoto okamžiku už je překladači jedno, jestli kompiluje C, nebo Pascal. Mezijazyk je podobný lispu. Tedy ideální pro strojové zpracování, protože všechny příkazy mají stejnou syntax (liší se pouze akcí) a obsahují dodatečně šikovné informace, jako třeba co potřebuje za vstup a výstup a co modifikuje. Všechny proměnné a paměť jsou unifikovány do registrů (třeba pole je také registr). Jazyk není reprezentován textově, ale ve speciální struktuře tak, aby se s ním dobře pracovalo. Je možné uložit jej do textové podoby.
Je pravda, že existují i jiné překladače, které mají mezikód, ale je to spíše vyjímka.
Následující přiklad:
main()
{
return(0);
}
Má mezikód
Potom se dostanou ke slovu optimalizátory. Valná většina je nezávislá na architektuře (procesoru), protože ani RTL se zatím moc asembleru nepodobá (snad až na jména registrů). Optimalizátorů je hodně. Takže jenom krátký popis:
zrušení skoků na skoky
tady se najde, kdy který registr (krycí název pro proměnnou) byl použit poprvé a naposled
ty mohou být vynechány
propagace konstant se snaží dosadit konstanty za registry. Odstraňuje výrazy bez výstupů a vedlejších efektů a vyhodnocuje výrazy bez vstupů. To výrazy zkracuje, vyhodí nepotřebné části programu atd.
Snaží se všechno, co jenom jde, dostat mimo smyčku. Znova proběhne eliminace expresion.
Rozdělí program do bloků, vyhazuje nedosažitelné smyčky, a zjišťuje, kde který registr je živý - to znamená, že je třeba uchovávat jeho hodnotu - najde výpočty, kde výsledek není použit.
(ne assemblerových, ale těch RTL) Spojuje instrukce, které jdou sjednotit do jedné, používá algebru na optimalizaci matematických výrazů a přidělí RTL kódu opravdové instrukce, které odpovídají popisu procesoru. (ano, tato část je částečně závislá na architektuře).
poskládá instrukce pro superskalární riscové procesory tak, aby bylo možné jich provádět více naráz. Verze 2.8.0 (která ještě není) to bude dělat i pro Pentia a Pentia pro.
hardwarové registry mají několik tříd. Tak třeba paměť (zásobník), registry procesoru, registry koprocesoru. Tato část rozhodne, které třídy registrů lze použít pro ten daný pseudoregistr.
tady se konečně rozhodne, které proměnné budou ve výsledném programu uloženy a kdy.
to samé, ale pro globální proměnné.
tady se dosadí hardwarové registry registrům a zbytek se uloží do zásobníku.
aby se instrukce nehádaly o paměť, potom se opakuje optimalizace skoků a ještě jednou scheduling
to je speciálně pro koprocesor i386
a dílo je hotovo.
Vyrobit assembler je už snadné: (Je v AT&T syntaxi . Ale snad to je jasné)
main:
xorl %eax,%eax
ret
A ještě jeden poněkud složitější příklad:
main()
{int i,a=1,b=0;
for(i=0;i<9999;i++)
a=a+b,b=2*a;
printf("%i\n",a);
}
K testům jsem se rozhodl použít výpočet Mandelbrotovy množiny. Je to netriviální, koprocesor využívající smyčka z reálného života. Tak se překladač má co činit.
Pro testy jsem použil Pentium/100 na 120, 512KB burst cache pod DOSem s himemem. Snažil jsem se všechny překladače donutit k co nejlepším výsledkům. Program jsem se snažil napsat co nejrychlejší.
Kompilátor | přepínače | Smyček za sekundu |
pgcc2.7.2p | -O6 -ffast-math -mpentium -frisc -fomit-frame-pointer -funroll-loops-fopt-reg-use -frisc | 5 464 480 |
egcs-1.0 | -O3 -ffast-math -mpentium -fomit-frame-pointer -funroll-loops | 5 405 405 |
egcs-1.0+HAIFA | -O3 -ffast-math -mpentium -fomit-frame-pointer -funroll-loops | 5 005 005 |
egcs-1.0 | -O3 -ffast-math -m486 -fomit-frame-pointer -funroll-loops | 4 807 692 |
gcc2.6.3 | -O3 -ffast-math -m486 -fomit-frame-pointer -funroll-loops | 4 233 641 |
gcc2.7.2.1 | -O3 -ffast-math -m486 -fomit-frame-pointer -funroll-loops | 419 377 |
gpc2.0 | -O3 -ffast-math -m486 -fomit-frame-pointer -funroll-loops | 3 433 962 |
vc5.0 | (optimalizace na max) | 3 060 908 |
wc10.0 | -7 -5 -ol -ol+ -om -op -or -ot -fp5 -fpi87 | 2 222 222 |
wc10.0 | -5 -7 | 2 217 249 |
delphi | max. speed | 1 807 288 |
plan9 | default | 1 623 376 |
vc1.0 | (v IDE-586,speed+size optim.) | 1 531 393 |
bc4.5 | (v IDE-486,fastest executable) | 1 455 604 |
bc3.1 | (v IDE-386,fastest executable) | 1 433 070 |
gcc2.7.2.1 | (bez optimalizací) | 1 281 348 |
gpc2.0 | (bez optimalizací) | 1 109 756 |
bp7.0 | max. speed | 901 713 |
tc2.0 | -1 -f87 -O -r -Z -G -a -mt | 846 511 |
bc4.0 | (v IDE-486,speed optimizations) | 755 857 |
bc2.0 | (bez optimalizací) | 716 535 |
bc2.0 | -G -O -2 -Z -r | 716 535 |
tc2.0 | max. speed | 546 546 |
bc4.0 | (v IDE-486,speed+size optim.) | -3.6676456... |
|
Jak je vidět, Borland C je 5.77krát pomalejší. Pascal je na tom o něco lépe, tedy 4.58 krát. Assemblerový zdroják se mi podařilo získat jenom z Borland C a GCC . Napočítal jsem 20 instrukcí v interní smyčce u GNU a 35 u Borland C. Novější Borland C se celkem slušně vylepšil. Ve verzi 4.5 je už ,,pouze'' 2.6krát pomalejší. Naprosto mě ale překvapil test ve verzi 4.0. Tam se zpomalil a po zapnutí optimalizací na velikost se program natolik zrychlil, že získal záporný čas. Nevím, jak někdo může kompilátor s tolika chybami pustit do světa. Další zajímavá věc ohledně Borland C je to, že ve verzi 3.1 byl .exe dlouhý 14KB, ve verzi 4.0 už 48KB a v 4.5 celých 61KB. V posledních Borland C jsem napočítal 34 instrukcí v interní smyčce. FractalZoom, který má smyčku v assembleru se pyšní 22 instrukcemi a jeho test říká 3.9 milionů smyček za sekundu. Zajímavé je, že staré GCC vyšlo z testu o 2.2% lépe. Je to tím, že jako nové DJGPP jako jediné počítá 18.2 tiků do sekundy místo 18. Watcom C přesto, že na rozdíl od GCC podporuje Pentium, schedulding instrukcí pro Pentium a fp optimalizace pro Pentia, bylo téměř dvakrát pomalejší. Ještě většího zrychlení dosáhla vývojová verze GCC - egcs pomocí unroolování smyčky, protože Pentiový koprocesor opravdu špatně snáší skoky. Při zapnutí optimalizací pro 480 se smyčka také unrooluje, ale proto, aby bylo méně fxch intrukcí, které na 486 zabírají čas(na pentiu už ne). Navíc konečná podmínka je zkombinvaná s počáteční dalšího testu, takže se ušetří jeden skok.
Provedl jsem ještě testy na SUNech a Siliconech GCC versus překladač dodávaný s OS. Výsledky jsou v tabulce 2 a 3
Kompilátor | přepínače | Smyček za sekundu |
gcc2.7.0 | -O3 -ffast-math -fomit-frame-pointer -funroll-loops | 925 925 |
cc | -O | 840 336 |
|
Kompilátor | přepínače | Smyček za sekundu |
gcc2.7.0 | -O3 -ffast-math -fomit-frame-pointer -funroll-loops | 2 688 172 |
cc | -O | 2 096 436 |
|
Musím říct, že výsledky mě opravdu překvapily, protože jsou mnohem vyrovnanější, než u DOSových překladačů. Napadly mě následující vysvětlení:
Proto jsem udělal další test. Nahradil jsem long double
za int
.
Teď je už celá matematika jednoduchá i na Intelu. Výsledky jsou
v tabulce 4.
Kompilátor | přepínače | Smyček za sekundu |
pgcc2.7.2p | -O6 -ffast-math -mpentium -frisc -fomit-frame-pointer -funroll-loops-fopt-reg-use -frisc | 3 464 480 |
egcs-1.0+HAIFA | -O3 -ffast-math -mpentium -fomit-frame-pointer -funroll-loops | 3 331 258 |
egcs-1.0 | -O3 -ffast-math -mpentium -fomit-frame-pointer -funroll-loops | 3 311 258 |
gcc2.7.2.1 | -O3 -ffast-math -m486 -fomit-frame pointer -funroll-loops | 3 250 000 |
wc10.0 | -fpi87 -fp5 -5 -7 -ol -ol+ -om -on -or -ot | 3246 753 |
wc10.0 | -5 -7 | 3 194 888 |
plan9 | default | 2 973 176 |
gpc2.0 | -O3 -ffast-math -fstrength reduce -fomit-frame pointer -funroll-loops | 2 888 888 |
gcc2.7.2.1 | (bez optimalizace) | 2 394 736 |
gpc2.0 | (bez optimalizace) | 2 219 512 |
bc2.0 | -G -O -2 -Z -r | 2 166 666 |
bp7.0 | max. speed | 1 956 947 |
tc2.0 | -1 -O -r -G -Z -a -mt | 892 156 |
tc2.0 | max. speed | 846 511 |
|
mul
pomalejší než fmul
a tak se výsledek asi dal očekávat.
Samozřejmě to neplatí o 486. Experimentální pgcc sice smyčku
trochu přerovná, ale pomůže to jen o cca 1%.
Pořád je ale Borland C o hodně pomalejší. To, že GCC by generovalo na mipsech horší kód se mi zdá nepravděpodobné, protože optimalizery jsou z velké části nezávislé na architektuře. A tak si myslím, že to bude tak půl na půl mezi lepší architekturou a tím, že UNIXové kompilátory bývají kvalitní.
Myslím, že testy mluví samy za sebe a ukazují, že tady GNU odvedlo opravdu dobrou práci. Psaní rutin v assembleru, aby byly rychlejší už skoro nemá smysl. Je to důvod, proč programy v Linuxu bývají rychlejší (klasický příklad je Doom). Má to bohužel i nevýhodu. Přežhavené stroje často programy kompilované pomocí GCC nevydrží (moje 486DX2 normálně pracující v pokojové teplotě se při kompilaci jádra zahřeje tak, že na ní neudržím ruku). Lidé si často myslí, že je to chyba programu. Také proces kompilace je docela složitý a tak to kompilátoru chvíli trvá.
Poslední dost důležitá věc je to, že oproti Borland C je GCC téměř bez chyb. Během šestiletého používání jsem narazil jen asi na tři problémy. Jinak vždy všechno fungovalo.
Dotazy a připomínky ohledně stránky posílejte na hubicka@paru.cas.cz