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

9. Rozšíření jazyka GNU C

Jazyk GNU C má některé užitečné rošíření oproti Ansi C, které se hodí hlavně, když chcete mít program hodně rychlý, a právě o nich se tu můžete něco dočíst. O rozšířeních G++ oproti C++ se tu zmiňovat nebudu.

9.1 Inline funkce versus built-in

Jazyk C se kompiluje jen do malé podmnožiny instrukcí počítače. Standard říká, že ostatní (jako třeba in, out, cli apod.) jsou příliš závislé na architektuře, než aby se dávaly do jazyka a jediná správná cesta je volat je z knihovních funkcí, které jsou psané v assembleru.

Bohužel často se tyto funkce používají ke zrychlení výsledku. Existuje například mnoho chytrých cest, jak kopírovat paměť (rep movsb na xt a 286, po 32bitových blocích na 386 a 486, přes koprocesor na pentiu..), které ani ten nejchytřejší překladač z jednoduchého for cyklu nevymyslí. Nebo jsou situace, kdy takové instrukce musíte používat často (třeba outin v hardwarových driverech) a jejich volání jako funkcí zdržuje, zvětšuje kód a přináší další potíže.

Jedna odpověď na to je built-in. Do překladače se zaintegrují nejčastější takové případy (jako je memset, outb apod.) a ten potom umí sám generovat optímální kód. Takových funkci je hodně (podívejte se do string.h a math.h například sin, cos, abs, fabs, strcpy, strdup, memcpy, memmove a takhle bych mohl pokračovat ještě hodně dlouho) řádově se to pohybuje podle mého názoru ve stovkách. Jednotlivé překladače se často triumfují v tom, kdo má více takových funkcí integrovaných, a některé - jako třeba Watcom C - jich umí opravdu hodně.

Na druhou stranu je to jenom jakási ošklivá výpomoc, která funguje jen v nejčastějších připadech. Představte si třeba, že jste vymysleli bezvadnou cestu jak implementovat násobení ve fixedpointu a chcete to použít ve svých programech. V knihovně taková funkce ale není a tak ani compiler ji nemá jako builtin. A máte smůlu. Takových případů jsou doslova tisíce - například memset pro 16ti a 32 bitová čísla, který se hodí pro truecolor apod. Navíc se to rozchází s filozofií C jako minimálního ale maximálně rychlého jazyka.

V GCC vývojáři zvolili jinou cestu. Builtin je opravdu jenom několik základních funkcí (abort, abs, alloca, cos, exit, fabs, ffs, labs, memcmp, memcpy, sin, sqrt, strcmp, strcpy, strlen) a to hlavně proto, že jsou natolik časté, že je třeba nejenom generovat optimální kód bez volání funkce ale třeba i předpočítávat jejich výsledek pro konstantu a dělat další podobné optimalizace specifické pro danou funkci. Místo toho ale do GCC přidali několik rozšíření tak, aby běžný programátor, aniž by měnil překladač, si mohl vytvořit svoje vlatsní funkce, které se chovají podobně jako built-in. Tyto rozšíření se navíc používají i v mnoha jiných případech. Je to:

9.2 Rozšížení šikovná hlavně pro optimalizaci

Mezi nejzajímavější rozšíření pro optimalizaci patří:

9.3 Ostatní

Gcc má mnoho dalších rozšížení jako je například:

Toto není ale kompletní seznam. GNU C obsahuje i další více či méně užitečné rozšíření. Rád bych více popsal ty podle mého názoru nejzajímavější.

9.4 Rozšířená syntax asm

Jako skoro každý překladač C i GCC má možnost vkládání inline assembleru. V GCC to je ale řešeno o dost odlišně. Většina lidí se toho děsí a ptá se, proč to u GNU neudělali normálně. Nevědí ale, že to bylo takto vymyšleno pro jejich dobro. Řešení v GCC má totiž několik výhod.

Na první pohled každého překvapí změněná syntax. Nejjednodušší použití ASM vypadá asi takto:

GCC: asm("cli");
BC:  asm { cli }
Proč to bylo takto uděláno? Výhoda je jednoduchá - nemate to programy, které znají C, ale neznají GCC. Pokud vidí zápis z GCC, řeknou si, že to je volání funkce a předání stringu, zatímco u verze z BC se několikráte podívají na asm a pak dojdou k záveřu, že tam chybí středník, to samé se opakuje u cli atd.

Dalším rozdílem je to, že GCC používá AT&T syntax assembleru. To samo osobě nepřináší žádnou výhodu ale ani nevýhodu. GCC funguje tak, že celý řetězec v asm prostě pošle dál assembleru a tak vůbec nic o assembleru vědět nemusí. To má tu výhodu, že například můžete použít MMX instrukce, pokud je umí assembler a nemusíte kvůli tomu shánět novou verzi GCC, která by pro MMX měla nějakou speciální podporu.

Navíc jsou programátoři, (jako já) kteří považují AT&T syntax za normální a nechápou jak mohli intelové tu jejich tak zkazit.

Nejdůležitější rozdíl ale je v optimalizaci. Pokud překladač uvidí jednoduchý zápis:

asm {
    mov ex,16
    mov cx,si
    int 17
}
už si nemůže být ničím jist - interrupt mohl klidně změnit všechny registry, globální proměné a ještě přerovnat zásobník. Prostě jeho představa o světě se zhroutí a nezbývá mu, než aby všechny snahy o optimalizaci vzdal.

Ale ani u jednodušších příkladů si nemůže být jist. Nemůže znát celou instrukční sadu (protože se stále objevují nové procesory a klony s novými instrukcemi - viz MMX), všechny vedlejší učínky, chyby v procesoru apod. a tak jednodušše nemůže nic pořádného o takovém kusu assembleru předpokládat. A to ani o samotné instrukci cli v minulém prípadě. Představte si, že píšete program, který často zapína a výpíná interrupty, uděláte si tedy inline funkce pro cli a sti:

static inline cli(void) { asm {cli}}
static inline sti(void) { asm {sti}}
Tyto funkce voláte z nejrůznějších interních smyček (což je celkem běžné u ovladačů), chudák překladač musí být zmaten a vyprodukovat strašný kód.

GCC je na tom lépe. Pokud u asm explicitně neřeknete, že něco mění, předpokládá se, že nemění nic. To jde tak daleko, že u funkcí:

static inline cli(void) { asm("cli");}
static inline sti(void) { asm("sti");}
dojde někdy dokonce k závěru, že když taková funkce nic nemění, je nejlepší ji vůbec nevolat, začne chytračit a volání vyoptimalizuje pryč (nebo alespoň odstraní ze smyčy, aby se to neprovádělo zbytečně často). Tomu se dá zamezit pomocí volatile. V ansi C je definováno, že když uvedete flag volatile u proměné, je nutné předpokládat, že má nějaky speciální význam (například je hlídána a měněna z časovače) a tak není možné na ní dělat některé optimalizace (jako předpokládat jakou bude mít hodnotu, ukládat každou chvíli jinam, vyhodit ji úplně apod.) U asm toto funguje podobně. Zápis:
static inline cli(void) { asm volatile ("cli");}
static inline sti(void) { asm volatile ("sti");}
už všechno bude fungovat tak, jak má, a kód bude optimalizován, jako kdyby tam žádné cli, nebo sti nebylo.

Ale pořád to není ono - optimalizace jde používat jen u některých hodně hloupých funkcí jako je cli, které nic nemění (ani registry) a nic nečtou (protože optimizer může usoudit, že se mu hodí váš assembler provédst jindy a funkci může celou přeházet). proto ani nemůžete předpokládat, že už všechno co jste napsali před asm je už hotové.

A proto má asm další rošíření. Za samotným stringem můžete napsat : a zadat, jaké má funkce výstupy, jaké vstupy a co modifikuje. To dá optimizeru pěkný obrázek o tom, co vlastně váš program dělá a může provádět další optimalizace.

Vstupní a výstupní parametry jsou v assembleru potom přístupné jako %0, %1 atd. (vstupy napřed, výstupy potom.) při kompilaci GCC potom projde string s assemblerem na % kombinace a nahradí je pravým umístěním proměné. Aby ale nedocházelo ke kolizím z očíslovanými registry, je nutné u asm ze vstupy a výstupy psát dvě % u registrů, tedy %%eax místo %eax. Například:

asm volatile ("outb %1, %0"
    :
    : "d" (port),
      "a" (data));
říká, že assembler má dva parametry (port a data), a ty nemění. Protože funkce má vedlejší účínek, který lze težko definovat, je nutné použít volatile. První dvojtečka říká, že assembler nemá žádné výstupy, další dvojtečka odděluje vstupy (to je port a data). magická kombinace "d"(port) se skládá ze dvou částí - třídy "d" a parametru (port) a říká, že proměná port má být uložena v registru edx. Druhý parametr oddělený čárkou má třídu "a" tedy registr eax. GCC podporuje mnoho tříd pro uložení dat. Základní jsou: Ale existují i další exotičtější třídy, kdo to myslí s psaním asm konstrukcí vážně by měl prostudovat manuál (info system). Například 'N' znamená konstantu 0--255, 'M' 0--3, 'O' je adresa, ke které jde přičítat offset apod. Je možně uvédst víc tříd naráz ("SD" znamená, že parametr má být v registru edi nebo esi)

Tento formát vychází ze způsobu, jakým GCC uchovává RTL instrukce a machine description (popis architektury).

Funkce out fungující i pro konstantí port je tedy:

       asm volatile ("outb %1, %0"
          :
          : "Nd" (port),
            "a" (data));
A ušetříte tím jeden registr a instrukci pro nastavování eax. Teď to vpodstatě říká, že instrukci outb jde používat buď pro konstantní port, nebo pro hodnotu uloženou v dx a pro data uložená v ax, což je přesně to, jak se out chová. U % parametrů je také možné přetypovávat, pokud není zaručeno, že parametry jsou toho správného typu. Třeba jde použít %b0 pokud to má být byte. Jsou podporovány následující typy: Navíc pokud potřebuje kód například registr ax nastavený na 1, je mnohem lepší uvédst ax mezi vstupy (a jako parametr napsat 1), než začínat kód příkazem movw $1,%ax, protože GCC tak může nějak jinak zařídit nastavení ax na 1 a ušetřit tak instrukci.

Za první dvojtečku se píše výstup - to je parametr, který musí být lvalue. GCC počítá s tím, že jeho hodnota se pouze zapisuje ale nečte. U třídy je nutné psát '=':

asm volatile ("inb %1, %0"
    : "=a" (rv)
    : "Nd" (port));
Toto načte z portu port hodnotu do proměné rv.

Pokud chcete vstupně výstupní proměné, můžete použít následující konstrukci:

asm ("incl %0": "=g" (i): "0" (i));
Toto provede i++ pro proměnou i uloženou kdekoliv. Podobnou řádku najdete i v souboru i386.md (machine description pro 386). "0" říká, že tento parametr musí být uložen na stejném místě jako parametr číslo 0 (výstupní i). Pokud to tam použijete "g", gcc nebude mít pocit, že to první i nějak souvisí s tím druhým a bude na ně nahlížet jako na dvě různé proměné a pro každou z nich může třeba zvolit jiný registr, podle toho, jak se to ve zbytku kódu hodí. Navíc gcc může výstupní parametry umístit na stejné místo jako vstupní, protože předpokládá, že vstupy se napřed načtou, pak se provede nějaké zpracování a potom se uloží do výstupů. Pokud váš kód mixuje vstupy a výstupy, je nutné k výstupní třídě přidat "&" jako v následujícím getpixelu:
asm (
      "movw %w1, %%fs
       .byte 0x64
       movb (%2, %3), %b0"
   : "=&q" (result)               /* výstup in al, bl, cl, nebo dl */
   : "rm" (seg),                  /* segment selector v reg, nepo paměti */
     "r" (pos),                   /* začátek řádky*/
     "r" (x)                      /* pozice na řádce*/
   );
Poslední důležitá věc je to, že občas takové assemblerové programy potřebují registry. Jedna z cest je na začátku uložit modifikované registry na stack a na konci vyzvednout. Není to ale nejlepší a GCC nabízí jinou cestu. Za poslední : můžete napsat seznam registrů, které jste modifikovali a "cc" pro změnu flagů. Pokud kód modifikuje a čte paměť nejakým podivným způsobem (jinak, než jsou jenom změny proměných), je nutné napsat i "memory". To zařídí, aby se všechny proměné uložily do paměti, než se kód provede a potom se předpokládalo, že se mohly změnit. Navíc je u asm statementů modifikujících paměť (například ekvivalent pro memcpy) často nutné používat volatile, protože paměť není vedena ani mezi vstupy ani mezi výstupy a tak optimizer nevidí důvod, proč by takovoý kód nemohl přemisťovat, vyhodit ze smyčky apod.

Například následující kód funguje jako memcpy a kopíruje n bytů ze src do dest (toto je ale jen ukázkový příklad a cesta přes rep movsb je velmi pomalá):

asm volatile (
  "cld
   rep
   movsb"
   :                               /*bez výstupních proměných*/
   :"c" (n),"S" (src),"D" (dest)   /*do cx počet, do si zdroj, di cíl*/
   :"cx","si","di","memory","cc"); /*modifikované registry, paměť a flagy*/
To je asi kompletní syntax. Možná vám není úplně jasné k čemu je takto obecná syntax nutná. Je to právě kvůli inlinování funkcí. Pokud píšete kus assembleru do svého kódu, je situace mnohem jednodušší - ušijete ho na míru dané situaci. Když ale děláte inline funkci, je lepší dát optimizeru větší volnost.

Nakonec jedno velké varování. naučte se pořádně tuto syntax, než začnete programovat. Je zdrojem častých chyb. Zapomenete na nějakou drobnost - třeba uvédst volatile a ono to fungovat může a nemusí. Také se může dost dobře stát, že to funguje ale jen 999 z tisíce pokusů, nebo tak, že to je nakonec pomalejší, než kdybyste to napsali v C. Nejčastější chyby jsou:

Pokud chcete, aby gcc kompilovalo vaše funkce bez řečí i v -pedantic módu, je nutné nepsat stringy na několik řádek a každou ukončit pomocí \n" a novou začít pomocí ". Vypadá to potom strašně, ale co se dá dělat. Staré C neumělo víceřádkové stringy. Také je možné používat __asm__ místo asm__volatile__ místo volatile.

Nakonec jenom několik chybých a neefektivních funkcí, které jsem při psaní tohoto článku náhodou objevil v různých zdrojácích. Nalezení nedostatků ponechám čtenáři jako jednoduché cvičení. Jak vidíte i velcí mistři se občas utnou (a občas jim to i projde).

extern inline void * memmove(void * dest,const void * src, size_t n)
{
register void *tmp = (void *)dest;
if (dest<src)
__asm__ __volatile__ (
        "cld\n\t"
        "rep\n\t"
        "movsb"
        : /* no output */
        :"c" (n),"S" (src),"D" (tmp)
        :"cx","si","di");
else
__asm__ __volatile__ (
        "std\n\t"
        "rep\n\t"
        "movsb\n\t"
        "cld\n\t"
        : /* no output */
        :"c" (n), "S" (n-1+(const char *)src), "D" (n-1+(char *)tmp)
        :"cx","si","di","memory");
return dest;
}
                   -- linux kernel, linux/include/asm/string-486.h
extern __inline__ void
outportb (unsigned short _port, unsigned char _data)
{
  __asm__ __volatile__ ("outb %1, %0"
          :
          : "d" (_port),
            "a" (_data));
}
                   -- djgpp, include/inline/pc.h
        int i = 0;
        __asm__("
          pushl %%eax\n
          movl %0, %%eax\n
          addl $1, %%eax\n
          movl %%eax, %0\n
          popl %%eax"
          :
          : "g" (i)
        );
          /* i++; */
                     -- tutoriál k assembleru djasm.html
Smutné je, že takových příkladů je všude habaděj a téměř každá asm konstrukce, na kterou se podívám je špatně. Já jsem napočítal minimálně 6 nedostatků v těchto příkladech a co vy?

9.5 extern inline

Konstrukce extern inline konstrukce umožňuje udělat rychlé náhražky knihovních funkcí. Pokud je zapnutá optimalizace a překladač narazí na funkci deklarovanou jako extern inline, všechny další volání se inlinují. Pokud je ale optimalizace vyplá, funkce se ignoruje a volá se standardní. Proto do headerů můžete sepsat svoje nejoblíbenější funkce, které by měly být rychlé a takový header pak volat všude, kde je třeba. Taková běžná extern inline funkce je:

extern inline void
outportb (unsigned short _port, unsigned char _data)
{
  asm volatile ("outb %1, %0"
          :
          : "d" (_port),
            "a" (_data));
}
Samozřejmě, že jde extern inline funkce používat i pro standardní C kód, nejenom assembleru.

9.6 __builtin_constant_p

Představte si, že chcete implementovat optimální memset. To jde udělat například:

extern inline void * memset(void * s, char c,size_t count)
{
asm volatile(
       "cld
        rep
        stosb"
        : /* no output */
        :"a" (c),"D" (s),"c" (count)
        :"cx","di","memory","cc");
return s;
}
časem ale zjistíte, že volání memset(x,0,4096), které je časté v jádře - nulujou se tím stránky - je neoptimální, protože nuluje byte po bytu, přesto, že by to šlo hned po čtyřech. Mnohem rychlejší je:
extern inline void * memset(void * s, char c,size_t count)
{
asm volatile (
       "cld
        rep
        stosl"
        : /* no output */
        :"a" (c+(c<<8)+(c<<16)+(c<<24)),"D" (s),"c" (count/4)
        :"cx","di","memory","cc");
return s;
}
To sice nefunguje pro počty nedělitelné čtyřma, ale jinak pracuje pěkně. Ale zase můžou existovat volání třeba memset(s,0,4), pro které je použití tohoto kódu vrhání atomových náloží na vrabce. Kdybychom mohli předpoklát, že count je konstanta, mohli bychom napsat následující podivnou funkci:
extern inline void * memset(void * s, unsigned long pattern, size_t count)
{
        pattern=((unsigned char)patter) * 0x01010101;
        switch (count) {
                case 0:
                        return s;
                case 1:
                        *(unsigned char *)s = pattern;
                        return s;
                case 2:
                        *(unsigned short *)s = pattern;
                        return s;
                case 3:
                        *(unsigned short *)s = pattern;
                        *(2+(unsigned char *)s) = pattern;
                        return s;
                case 4:
                        *(unsigned long *)s = pattern;
                        return s;
        }
##define COMMON(x) \
asm ("cld; rep ; stosl" \
        x \
        : /* no outputs */ \
        : "a" (pattern),"c" (count/4),"D" ((long) s) \
        : "cx","di","memory","cc")

        switch (count % 4) {
                case 0: COMMON(""); return s;
                case 1: COMMON("\n\tstosb"); return s;
                case 2: COMMON("\n\tstosw"); return s;
                case 3: COMMON("\n\tstosw\n\tstosb"); return s;
        }

}
Toto funguje tak, že optimizer, který už bude vedět hodnotu count a pattern sám předpočte násobení na začátku a vybere tu správnou větev ve switch. A tak tento memset bude fungovat velmi rychle pro všechny konstantní patterny a počty.

Jediný problém je, že bychom potřebovali memset pro konstantní parametry a memset pro nekonstantní a nutit programátora, aby sám dával pozor na to, co je konstanta a co není (někdy to vůbec není jednoduché, hlavně když parametr je sice proměná, ale je možné předpočítat její hodnotu)

K tomu slouží právě __builtin_constant_p. Ta vrátí 1, pokud parametr je konstanta a jinak 0. Můžeme tedy napsat memset pro konstantní a nekonstantní parametry a potom vybrat ten správný pomocí:

#define __constant_c_x_memset(s, c, count) \
(__builtin_constant_p(count) ? \
 __constant_c_and_count_memset((s),(c),(count)) : \
 __constant_c_memset((s),(c),(count)))

#define __memset(s, c, count) \
(__builtin_constant_p(count) ? \
 __constant_count_memset((s),(c),(count)) : \
 __memset_generic((s),(c),(count)))

#define memset(s, c, count) \
(__builtin_constant_p(c) ? \
 __constant_c_x_memset((s),c,(count)) : \
 __memset((s),(c),(count)))
Zkušenému céčkaři už možná vstávájí vlasy na hlavě a ptá se: a co memset(s,c++,count)? kolikrát se c zvětší? Ale ani on nemusí mít obavy - parametry funkce __builtin_constant_p se nevyhodnocují takže i program:
main()
{
   int a = 1;
   __builtin_constant_p(a++);
   __builtin_constant_p(a++);
   printf("%i\n", a);
}
vypíše jedna. Tento memset je plnohodnoutnou náhražkou builtinu do překladače a má tu výhodu, že každý jej může upravovat podle svých potřeb - na velikost, rychlost, přidat další spec. připady, pro různé CPU apod.

Je nutné poznamenat, že __builtin_constant_p nepatří zrovna k nejspolehlivějším. Její hodnota se určuje před propagací konstant (v té době je už nutné vědět, kudy se program vydá) a tak i v předchozím případě bude vracet 0, protože parametrem je proměná (přesto, že při propagaci konstant se přijde na to, že její hodnota je 1). Z toho důvodu nemá smysl používat tuto funkci na parametry inline funkcí a je nutné psát makra. Proto používejte tutu metodu jen pokud to je nutnré.

9.7 Attributy funkcí

K optimalizaci se hodí jakákoliv informace. V některých případech je celkem těžké, aby optimizer zjistil některé speciality a proto mnoho překladačů má možnost přidat k deklaracím funkce i některé přídavné attributy - například to, že funkce se nikdy nevrátí (exit) a tak není třeba po jejím volání dále překládat. Většina z nich to ale řeší pomocí konstrukce #pragma. To přináší časté potíže s preprocesorem - nejde zahrnovat do maker apod. Protože je #pragma neportabilní nelze tudíž tyto věci elegantně přidat do portabilních programů.

GCC má poněkud jiné řešení - pomocí __attribute__, který se píše za deklaraci funkce. Nastavení takového attributu potom vypadá:

void ahoj(void) __attribute__(const);
Pokud chcete mít program přenositelný, stačí přidat do nějakého headeru konstrukci:
#ifdef __GCC__
# define CONST(f) f __attribute__const
#else
# define CONST(f) f
#endif
a potom používat pouze:
void CONST(ahoj(void));
Jsou k dispozici následující attributy:
noreturn

říká, že funkce se nikdy nevrátí - jako např. exit.

const

funkce nedělá nic jiného, než že se koukne na své parametry a z nich vyvodí výsledek, bez dalších vedlejších efektů, nebo prohlížení paměti. Compiler potom dělá stejné optimalizace jako na operátor. Příkladem takovych funkcí je například abs, sin, cos apod. Například funkce rand to už není.

regparm(počet)

Prvních počet parametrů se bude předávat v registru. To urychlí volání funkce. GCC umí předávat v registrech i defaultně, je ale nutné pro to překompilovat knihovny.

constructor

funkce se zavolá před provedením main.

destructor

funkce se zavolá před skončením programu.

stdcall

funkce bude používat pascalácké volací konvence.

cdecl

použijí se C konvence, pokud pascalácké volání je jako default.

format(typ,format,odkud)

Funkce typu printf a scanf nemají žádnou kontrolu typů. To je zdrojem častých chyb a proto GCC umí tyto typy kontrolovat. Pokud uvedete tento attribut u své funkce, která má stejné konvence, GCC to bude kontrolovat i tam. Typ může být printf nebo scanf.

unused

Compiler nebude vypisovat warning, kdyz funkce je neppoužita.

weak

Weak je něco jako static nebo global. Pokud v knihovně uděláte globální proměnou, je vidět zvenku a aplikace ji může omylem přepsat. Pokud je ale weak, je lokální pro danou knihovnu.

alias(fce)

Funkce je pouze alias pro funkci fce

section(jmeno)

Funkce se uloží do jiné sekce - linker potom může tyto sekci dát do jiné části výsledku a tak jdou dělat věci jako, že na konci je inicializační kód, který se po startu uvolní z paměti.

9.8 Některá rozšíření šikovná (nejenom) pro psaní maker

Celkem častno se stává, že chcete napsat makro, které jde napsat na místo funkce, ale potřebujete tam proměnou, nebo smyčku apod. a proto jako výraz napsat nejde. Proto má GCC {( )}, což je konstrukce, která převede libovolný blok do výrazu. Jeho hodnota je potom hodnota posledního vyhodnoceného výrazu. Proto jde napsat makro MAX tak, aby se parametry vyhodnocovaly jen jednou:

#define MAX(a,b) ({int _a=(a), _b=(b); _a > _b ? _a : _b })
Toto makro se chová o něco lépe, než extern inline funkce se stejným kódem, protože inline funkce se napřed optimalizují odděleně a po inlinování funkce se provedou už jenom některé optimalizace a tak makra jsou stále o něco lepší pro optimizer.

Této konstrukce lze také z výhodou vužít v kombinaci s asm:

#define plus(a,b) ({int _a=(a) _b=(b), asm("add %0 %1":"=g" (_a):\
                   "0" (_a):"g" (_b):"cc");_a}
Klasické makro pro MAX má sice nevýhodu, že své parametry vyhodnocuje několikrát, ale zase funguje i pro jiné typy (třeba pro double. K tomu aby i nová verze makra MAX chodila pro libovolný typ slouží konstrukce typeof, která nadefinuje typ podle expression dané jako parametr.
#define MAX(a,b) ({typeof _ta=(a), _tb=(b); \
                         _ta _a=(a); _tb _b(b); \
                         _a > _b ? _a : _b })
Další šikovná věc je makro s proměným počtem parametru:
#define eprintf(f,a ...) fprintf(stderr, f, ## a);
Některé zhýralce možná napadlo psát do makra labely. Ale nejde to, protože když použijete makro dvakrát ve stejné funkci, máte tam i dva stejne labely a kompiler to neveme. Proto můžete na začátku bloku napsat __label = jméno a nadefinovat label jako lokální pro daný blok.

9.9 Explicitní určování registrů pro proměné

Některé překladače umožňují určit registr pro proměnou tak, že ji pojmenujete například __ax. To ale přináší problémy s portabilitou. Pokud chcete, aby na jedné architektuře byla proměna v registru ax a na jiné v r1, musíte ji pokaždé pojmenovat jinak. Také hrozí možnost náhodné kolize.

GCC má opět jiný přístup k věci. Pokud chcete uložit proměnou do registru napíšete například:

      register int ahoj asm("ax");
Gcc umí i velice zajímavou věc - dělat takové proměné globálně. To funguje docela dobře, protože běžně funkce potom co taková proměná byla deklarována registr nepoužívají a mohou tedy velmi rychle pracovat s danou proměnou. Knihovní funkce, které ale nic o této proměné neví, registr normálně uloží na zásobník a potom zase vyzvednou, takže se jeho hodnota neztratí. Není přístupná pouze v případě, že vaše funkce je volána z knihovní (qsort).


Předchozí Následující Obsah

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