Kompilátory CMartin Mareš
V dnešní uspěchané době téměř každý programátor tvoří své programy v některém z
vyšších programovacích jazyků, protože práce v assembleru je přeci jen zdlouhavá
a výsledné programy nejsou přenositelné mezi různými architekturami počítačů.
Ale jsou vyšší jazyky oproti assembleru vždy pouze ve výhodě? Podívejme se nyní
na tento problém trochu podrobněji... Co to?
Počítače a programování jsou tu přeci jen již nějaký ten pátek, a tak za ta
léta spatřily světlo světa nesčetné programovací jazyky - některé z nich jako
meteory na nebi jen krátce zazářily a poté upadly v zapomnění, jiné přetrvaly
věky a užívají se dodnes. Na Amigách dosáhly zvláštní obliby jazyky C a E. C je
jazyk vpravdě staroslavný (existoval již za časů, kdy i UNIX byl novinkou),
naproti tomu jazyku E není víc než pár let a dosud na něj bohužel nejsou k
dispozici kvalitní kompilátory. Zaměříme se tedy na jazyk C, jehož kurs máte
ostatně možnost sledovat na stránkách tohoto časopisu a jeho nejznámější
amigovské kompilátory: SAS/C a GCC.
Tento článek si klade za cíl ukázat na několika příkladech, jak takový
kompilátor pracuje a jak dobrý výsledný kód ve srovnání s ručně psaným
assemblerským programem od něj lze očekávat. Nejedná se nám ovšem o podrobnou
recenzi popisovaných kompilátorů nýbrž pouze o jakýsi stručný úvod který by mohl
posloužit těm, kteří se dosud nerozhodli, zda programovat v assembleru nebo v
nějakém vyšším jazyku. Na start, prosím
Nechť tedy soupeři zaujmou místa na startovních blocích a my si mezi tím
trochu popovídáme o tom, co jsou zač:
SAS/C 6.55 (ve starších verzích se jmenoval Lattice C) je jeden z nejstarších
kompilátorů jazyka C na Amigu, což mu ale nikterak neubírá na kvalitě, poněvadž
po ta léta byl svými autory pečlivě vylepšován. Jeho výhodou je, že vznikal
přímo pro Amigu, a tudíž si s jejím operačním systémem rozumí téměř dokonale. Na
druhou stranu, jeho verze 6.0 až 6.2 smutně prosluly velkým množstvím chyb - v
mnoha zdrojových textech amigovských programů se dočtete, že chcete-li je
kompilovat pomocí SAS/C, musíte použít verzi 6.50 nebo novější, neboť v opačném
případě program po zkompilování nefunguje. SAS/C podporuje generování
optimalizovaného kódu pro všechny procesory řady 68000 až do 68040 včetně
koprocesorů 68881 a 68882. Při následujících testech byl nastaven na procesor
68000 a všechny optimalizační přepínače (PARAMETERS=REGISTERS, NOSTACKCHECK,
STRINGMERGE, OPTIMIZE, OPTIMIZERSIZE, OPTIMIZERINLINELOCAL a UTILITYLIBRARY)
byly zapnuty.
GCC 2.6.3 není původem amigovský kompilátor vznikl v prostředí VAXů a UNIXových
systémů a na Amigu byl poté převeden. U kolébky tohoto programu stál Richard M.
Stallman z Massachusetského Technologického Institutu (MIT). V té době již mnozí
počítačoví experti na celém světě používali jeho editor EMACS, který fungoval na
většině tehdy provozovaných počítačů, byl přenositelný i na jiné v té době
vyvíjené, velmi dobře jej bylo možno rozšiřovat o další funkce a co hlavně -
jeho autor ho poskytuje ostatním zcela zdarma, čímž otevřel dokořán dveře všem,
kteří měli zájem EMACS dále vylepšovat. Po tomto (dnes již legendárním) úspěchu
se R. M. Stallman rozhodl pro věc tehdy velice odvážnou (a ostatně, pokud vím,
dodnes nikým jiným nezopakovanou): napsat kompilátor jazyka C založený na stejné
filosofii jako EMACS - všude fungující, generující co možná nejlepší kód a zcela
volně distribuovatelný. Po několika letech byla tato snaha korunována úspěchem a
GCC vyrazilo na vítězné tažení po celém tehdejším počítačovém světě, neboť jej
bylo velice snadné naučit vytvářet kód pro mnoho různých architektur. Ke
Stallmanovu úsilí se připojilo mnoho dalších programátorů a dodělávali do GCC
různá další vylepšení, a tak GCC dnes podporuje již 17 různých řad procesorů
(Motorola 680x0, VAX, Sparc, Convex, AMD 29000, ARM, Motorola 88000 IBM RS/4000,
PowerPC IBM RT, MIPS, Intel 80x86, HP PA-RISC, Intel 80960, DEC Alpha, Clipper a
H8/300). Na Amize jsou podporovány všechny procesory od 68000 do 68040 včetně
koprocesorů a připravuje se i podpora pro 68060. V našich testech jsme
kompilovali pro 68000 s plnými optimalizacemi (přepínač -O2). Pravděpodobně
jedinou podstatnou nevýhodou GCC je, že k rozumnému používání vyžaduje 5 MB RAM
a systém 2.04 nebo novější.
A na třetí startovní blok se mírně nejistě vyhoupl jakýsi človíček poněkud menší
postavy, rozpačitě se usmál na okolo stojící diváky a začal si pohvizdovat
jakousi smutnou melodii. Že ho neznáte? Nedivte se - on je totiž programátor v
assembleru a těch už dneska po tom našem světe tolik nechodí. Ale přece jen
existují, neboť i nyní jsou věci, které prostě nikdo jiný nezvládá napsat: jádra
operačních systémů, zázračné patche opravující podivné chyby hardwaru apod.
Nechme jej nyní jeho vlastnímu světu počítačové magie a podívejme se trochu
jinam. Jak pracuje kompilátor
Takový kompilátor jazyka C není vlastně nic jednoduchého. Bývá zpravidla
složen z několika základních modulů, někdy rozdělených do jednotlivých programů,
jindy spojených do jednoho programu velkého, což však na jejich funkci nic
nemění.
Na počátku stojí zdrojový text programu, právě opustivší textový editor (dost
možná i mnoho různých zdrojových textů, z nichž se váš rozsáhlý program skládá).
Tento text je nejprve zpracován preprocesorem jenž rozvine v textu všechna vámi
definovaná makra ("#define"), nahradí všechny příkazy "#include" obsahy
příslušných souborů, odstraní komentáře a zpracuje podmíněné překlady, čímž
vytvoří jeden soubor obsahující "čistý" zdrojový text bez jakýchkoliv direktiv.
Tento text je dále zpracován takzvanou první fází kompilace. Ta jej postupně
analyzuje na jednotlivé lexikální elementy (čísla, identifikátory operátory,
závorky, řetězce, středníky atd.). Z těch sestavuje stále větší a větší
syntaktické celky, až má hotový příkaz. Ten pak přeloží do takzvaného mezikódu
to je velice podivný programovací jazyk sestávající z malého počtu primitivních
operací, nikoliv nepodobným assemblerským instrukcím, ale stále zde ještě
existují výrazy, příkazy cykly a jiné konstrukce známé z vyšších jazyků.
Jednotlivé příkazy jsou postupně skládány k sobě, až má kompilátor pohromadě
celou funkci.
Každá funkce je nyní rozvinuta do tzv. lineárního programu, který obsahuje pouze
jednoduché sekvenční operace (přiřazení a vyhodnocení výrazu, skoky podmíněné a
nepodmíněné, volání podprogramů a návraty z nich). Všechny složitější řídící
struktury (cykly a podmínky) jsou rozloženy na tyto základní elementy.
V tomto tvaru započne to nejdůležitější: optimalizace programu. To je fáze
neobyčejně komplikovaná, skládající se z mnoha různých dílčích průchodů, v nichž
se optimalizátor postupně snaží odstranit nejrůznější neefektivity v programu,
umístit co možná nejvhodněji proměnné do registrů procesoru, vyčíslit konstantní
podvýrazy a provést jiné triky potřebné k tomu, aby program byl co nejkratší a
nejrychlejší. Na konci nám zbude program zapsaný v přesně tomtéž mezikódu, ale
nepoznání změněný.
Optimalizovaný mezikód (nebo také neoptimalizovaný, pokud si uživatel nepřál
optimalizaci provést - kupříkladu proto, že to ještě není finální verze programu
a on chce mít kompilaci hotovou co nejdříve) je dále postoupen ke zpracování
druhé fázi kompilace (ona vlastně, jak vidíte, není druhá v pořadí, ale "druhá
fáze" je historický termín, jejž je zvykem pro tuto činnost používat dodnes).
Nyní je mezikód postupně zjednodušován a upravován, až se z něj stanou instrukce
assembleru. Ty jsou zapsány do souboru s výsledkem kompilace (pravda, zatím jen
assemblerským, ale jsme již blízko u cíle) a jdeme se utkat s další funkcí -
první fáze pokračuje.
Když již máme hotový celý program v assembleru, zkompilujeme assembler do
strojového kódu každou instrukci nahradíme jejím kódem. Oproti předchozím akcím
dokonale triviální, že? A máme tzv. object file (česky se někdy překládá jako
objektový soubor, ale to se příliš neujalo). Ten sice ještě není možno spustit,
ale již je to v podstatě funkční kus programu.
Na závěr přichází na scénu linker a spojí všechny zkompilované object files
(získané z různých zdrojových textů) spolu s knihovními funkcemi (pokud
nepoužíváte sdílené knihovny amigovského systému, které se natahují až za běhu
programu) do cílového spustitelného souboru. Představení skončilo.
Zvídavé čtenáře jistě napadne, jaký že má smysl rozdělovat program na více
zdrojových textů a proč že se kompiluje po jednotlivých funkcích, když by přeci
bylo přirozenější (a vedlo by k lepším výsledkům) při optimalizaci na něj hledět
jako na celek. Odpovědi jsou snadné: Rozdělení programu do menších relativně
nezávislých modulů přinese značné zrychlení kompilace (při malé změně v programu
se znovu kompiluje pouze ten modul, v němž byla změna provedena, ostatní se k
němu pouze znovu přilinkují). A optimalizace se děje po funkcích pouze proto, že
je to velice časově i paměťově náročná činnost, neboť pro každou operaci v
programu je nutno ukládat veliké množství pomocných informací a zkoumat její
vztahy vzhledem ke všem ostatním instrukcím. Kdyby se tak zpracovával celý
program najednou, čekali bychom na výsledek do soudného dne a ještě bychom se
hodinu před kýženým dokončením kompilace dozvěděli, že oněch 64 MB paměti, co
jsme kompilátoru dali k dispozici, bylo příliš málo.
Zbývá ještě podotknout, že výše uvedený popis "sedí" naprosto dokonale pouze na
GCC. Ostatní kompilátory pracují v detailech jinak (například SAS/C má assembler
zabudovaný přímo v sobě, takže assemblerský text vlastně ani nevznikne), leč
základní idea celého postupu je vždy stejná. Klání začíná
Zadejme nyní naším soutěžícím jeden jednoduchý příklad na zahřátí: int main(void)
{
int a,b,c;
a = 1;
b=2;
c = a+b
return 128;
} Assemblerský programátor si ihned všiml, že funkce vlastně vůbec nic nedělá a
pouze vrací v registru D0 hodnotu 128, a tak napsal: move.l # 128,d0
rts Oba kompilátory prohodily na adresu autora programu pár peprných slov
(warning nebo též hromburác) o tom, že proměnné se nikde nepoužívají, a po malé
chvíli vyrobily program ve znění: moveq #127,d0 ; SAS/C
not.b d0
rts
moveq #64,d0 ; GCC
add.l d0,d0
rts Na první pohled se zdá, že oba kompilátory vyrábějí hodnotu výsledku daleko
složitějším způsobem, než by bylo zdrávo. Ale ouha, jejich výsledný kód je o 2
byty kratší. Tedy v tomto testu obstály na výbornou a náš assemblerista se zase
něco přiučil (to kompilátory bohužel nemohou).
Nu dobrá, na program, co nedělá nic, se nedal nachytat nikdo. Trochu ho tedy
upravíme: před return vsuneme printf("%d
",c), aby program alespoň něco dělal.
A assemblerista na to (aby byl souboj spravedlivý, požádali jsme jej, aby
používal stejné knihovní funkce, jelikož o ty nám dnes nejde): lea txt(pc),a0 ;@puts vypise string, parametrem
bsr @puts ;je jeho adresa v registru A0
moveq # 127,40 ;A vracíme hodnotu (již jsme se
not.b d0 ;naučili, jak to udělat efektivně)
rts
txt dc.b 3,0 ;Toť náš řetězec Ejhle, to vypadá pěkně. Žádný formátovaný tisk, ihned se vše vypisuje jako
hotový řetězec. Kompilátory ovšem nemají ani páru o tom, jak "printf" funguje,
je to pro ně prostě knihovní služba jako každá jiná. Následující výpis ukazuje
výtvor SAS/C (GCC řešilo nastalou situaci obdobně): pea 3.w ;Přímo dosazený výsledek výrazu
pea txt(pc) ; "%d
" (parametry na zásobníku)
bsr _printf ;Volání printf
addq.l #8,sp ;Odstraníme parametry
moveq #64,d0 ;Jako minule
add.l d0,d0
rts
txt dc.b %d,10,0 Zde vidíme, že kompilátory bohužel nemají vlastní inteligenci a nejsou
schopny hloubat o tom, co která vnější funkce dělá, a tudíž poctivě volají
printf s parametry na zásobníku (v registrech to bohužel nejde, neboť jich je
obecně proměnlivý počet). K dobru jim však budiž přičteno, že ve zdrojovém textu
uvedené proměnné vůbec nezaváděly a pouze vypočtenou hodnotu dosadily jako
konstantní parametr funkce printf. Druhé kolo
Zadejme nyní něco o trochu složitějšího, kde se toho již děje o ždibec více.
Budeme 64-krát vypisovat slavný text "Hello, world!": int main(void)
{
int i;
for(i=0; i<64; i++)
puts("Hello, world!");
return 0;
} Jak jednoduché. A dá se na tom vůbec něco pokazit? Uvidíme. Následuje
výsledek SAS/C. GCC vyrobilo téměř identický kód, až na to, že parametr funkce
"puts" předává přes zásobník neboť současná verze knihoven amigovského GCC
nepodporuje předávání skrze registry. move.l d7,-(sp) ;Uložíme si registr
moveq #0,d7 ;A jedeme: i=0
lab:
lea txt(pc),a0 ;puts("Hello, world!")
bsr @puts
addq.l #1,d7 ;i++
moveq #64,d0
cmp.l d0,d7
blt.s lab ;i<64 => skáčeme!
moveq #0,d0 ;return 0
move.l (sp)+,d7 ;Obnovíme registr
rts ;A končíme.
txt: dc.b Hello, world!,0 Assemblerista neváhá a řeší problém po svém: move.l d2,-(sp) ; Uklidíme původní obsah registru
moveq #63,d2 ; Počet opakování minus 1
lab:
lea txt(pc),a0 ; puts("Hello, world!")
bsr @puts
dbf d2,lab ; Cyklus
move.l (sp)+,d2 ; Obnovíme obsah registru
moveq #0,d0 ; return 0
rts ; A konec
txt: dc.b Hello, world! ,0 Vida, to je o poznání jednodušší a kratší. Autor si totiž uvědomil, že nám
vůbec nejde o to, provést něco pro všechny hodnoty proměnné od 0 do 63, ale
provést to 64-krát (ta proměnná se přeci nikde uvnitř cyklu nepoužívá). Opět
malé vítězství. Co to? Zastánci kompilátorů na tribunách pískají a řvou, že tak
to přeci v zadání nebylo - původní program přeci neměl počítat pozpátku a pouze
16-bitově! My ovšem dobře víme, jak jsme zadání mysleli: chtěli jsme jenom
vypsat něco stokrát, ale v zájmu spravedlnosti zadání trochu přeformulujeme a
necháme přeložit znovu: int main(void)
{
short i;
for(i=63; i>=0; i--)
puts("Hello!");
} A výsledek: move.l d7,-(sp) ; SAS/C
moveq #63,d7
lab:
lea txt(pc),a0
bsr @puts
subq.w #1,d7
bpl.s lab
moveq #0,d0
move.l (sp)+,d7
rts
txt: dc.b Hello, world!,0 move.l d2,-(sp) ; GCC
moveq #63,d2
lab:
pea txt
jsr _puts
addq.l #4,sp
dbf d2,lab
moveq #0,d0
move.l (sp)+,d2
rfs
txt: dc.b Hello, world!,0 K těmto programům jistě netřeba podrobných komentářů. Pouze si povšimněte, že
SAS/C použilo "doslovného" překladu cyklu, zatímco GCC cyklus vyřešilo (stejně
jako náš assemblerista) instrukcí dbf, která je stejně dlouhá, ale rychlejší a
elegantnější než první řešení, a že GCC opět užilo méně efektivní zásobníkové
parametry, stále znovu a znovu na zásobník přidávané a z něj odstraňované.
Kompilátory C se již prakticky vyrovnaly ručně psavému assemblerskému programu,
ale uznejte, kdo z vás by to při programování v C zrovna takhle napsal?
(Dokončení příště) Vytlačiť článok
Pozn.: články boli naskenované ako text a preto obsahujú aj zopár chýb. Taktiež neručíme za zdrojové kódy (Asm, C, Arexx, AmigaGuide, Html) a odkazy na web. Dúfame, že napriek tomu vám táto databáza dobre poslúži.
Žiadna časť nesmie byť reprodukovaná alebo inak šírená bez písomného povolenia vydavatela © ATLANTIDA Publishing
none
|