Logo GNU
Kodovani Předchozí Následující Obsah

4. Překladač

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:

Optimalizace skoků

zrušení skoků na skoky

Scanování registrů

tady se najde, kdy který registr (krycí název pro proměnnou) byl použit poprvé a naposled

Optimalizace skoků na stejný nebo opačný test

ty mohou být vynechány

Propagace konstant, eliminace výrazů

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.

Optimalizace smyček

Snaží se všechno, co jenom jde, dostat mimo smyčku. Znova proběhne eliminace expresion.

Analýza toku dat

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.

Kombinace instrukcí

(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).

Scheduling instrukcí

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.

Třídy registrů

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.

Alokace registrů

tady se konečně rozhodne, které proměnné budou ve výsledném programu uloženy a kdy.

Globální alokace registrů

to samé, ale pro globální proměnné.

Reloading

tady se dosadí hardwarové registry registrům a zbytek se uloží do zásobníku.

Další scheduling

aby se instrukce nehádaly o paměť, potom se opakuje optimalizace skoků a ještě jednou scheduling

optimalizace zásobníkových registrů

to je speciálně pro koprocesor i386

Převod do assembleru

a dílo je hotovo.

Náš příklad po optimalizaci

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);
      }

Výsledek z  GCCBorland C.

4.1 Srovnání s ostatními

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.

Zdrojový kód v  C, a v  Pascalu

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...
Srovnání rychlostí na Pentiu ve floating point verzi
Vc znamená Visual C++ 1.0/5.0. GCC 2.6.3 bylo použito v DJGPP 1.0. Wc znamená Watcom C 10.0. Plan9 je kompilátor ve stejnojmenném OS (který je mimochodem také velice zajímavý). Gpc je GNU Pascal compiler. Egcs je odštěpená větev od vývoje GCC 2.8.0, má ale navíc některé optimalizace a nový HAIFA scheduler od IBM. Má za ůkol vytvořit GCC 3.0.0, které by mělo mít výrazně lepší podporu superskalárních procesorů, zejména Pentia, PentiPro, Pentia II, UltraSparcu, Alphy a PowerPC. Ma také několik nových optimalizačních průchodů (napřiklad globání eliminaci výrazů). PGCC je GCC pro Pentium, ktere původně začal vyvíjet Intel. Potom co dosáhl 30% urychlení ve smíšených benchmarkách, nechal vývoje a uvolnil zdrojáky. Vývoje se ujala skupina jménem Pentium Support Group, která odstranila velkou část chyb, které Intel zavedl, přidala podporu PentiaPro, k5, k6 a Cyrixe. Použitelná část změn v PGCC bude brzo přidána do EGCS. Problém PGCC je na GNU poměry vysoká nestabilita a to, že v některých případech oproti starému GCC zpomaluje, proto přidávání změn do EGCS pravděpodobně bude chvíli trvat.

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 CGCC . 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
Srovnání rychlostí na SUNovi
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
Srovnání rychlostí na SGI

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
Srovnání rychlostí integerové verze
Výsledek mě opět překvapil. Zpomalení u GCC jsem nečekal. Po prozkoumání assembleru jsem zjistil, že tam není nic, co bych byl schopný nějak radikálně zoptimalizovat - všechno je v registrech a smyčka se zkrátila na 14 instrukcí. Pentium má 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.


Předchozí Následující Obsah

Dotazy a připomínky ohledně stránky posílejte na hubicka@paru.cas.cz