AMIGA REVIEW online
  Uvodná stránka     Software     Hry     Obaly     Download     Amiga na PC     Amiga Forever  

Kompilátory C

Martin 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


© ATLANTIDA Publishing Všechna práva vyhrazena.
Žádna část nesmí být reprodukována nebo jinak šířena bez písemného svolení vydavatele.



Amiga na Vašem PC rychle, snadno a zdarma!


none

AMIGA REVIEW

57 ( 11-12 / 2000 )
56 ( 9-10 / 2000 )
55 ( 7-8 / 2000 )
54 ( 5-6 / 2000 )
53 ( 3-4 / 2000 )
52 ( 1-2 / 2000 )
 
51 ( 12 / 1999 )
50 ( 11 / 1999 )
49 ( 10 / 1999 )
48 ( 9 / 1999 )
46-47 ( 7-8 / 1999 )
45 ( 6 / 1999 )
44 ( 5 / 1999 )
43 ( 4 / 1999 )
42 ( 3 / 1999 )
41 ( 2 / 1999 )
40 ( 1 / 1999 )
 
39 ( 12 / 1998 )
38 ( 11 / 1998 )
37 ( 10 / 1998 )
36 ( 9 / 1998 )
35 ( x / 1998 )
34 ( x / 1998 )
33 ( 1-2 / 1998 )
 
32 ( 11-12 / 1997 )
31 ( 9-10 / 1997 )
30 ( 7-8 / 1997 )
29 ( 6 / 1997 )
28 ( 5 / 1997 )
27 ( 4 / 1997 )
26 ( 3 / 1997 )
25 ( 2 / 1997 )
24 ( 1 / 1997 )
 
23 ( 12 / 1996 )
22 ( 11 / 1996 )
21 ( 10 / 1996 )
20 ( 9 / 1996 )
18-19 ( 7-8 / 1996 )
17 ( 6 / 1996 )
16 ( 5 / 1996 )
15 ( 4 / 1996 )
14 ( 3 / 1996 )
13 ( 2 / 1996 )
12 ( 1 / 1996 )
 
11 ( 12 / 1995 )
10 ( 11 / 1995 )
9 ( 10 / 1995 )
8 ( 9 / 1995 )
7 ( 7 / 1995 )
6 ( 5 / 1995 )

ATLANTIDA NEWS

5 ( 3 / 1995 )
4 ( 1 / 1995 )
 
3 ( 11 / 1994 )
2 ( 9 / 1994 )
1 ( 7 / 1994 )
0 ( 5 / 1994 )