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.
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 out
a in
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:
__builtin_constant_p
Mezi nejzajímavější rozšíření pro optimalizaci patří:
Gcc má mnoho dalších rozšížení jako je například:
ukazatel = &&label
, goto *ukazatel
)char *
)case 1 ... 9:
)typedef cislo = (1/2)
)(a ? b : c) = 5
)x ? : y
je stejné jako x ? x : y
)int a[6]={ [4] 29, [2] 15};
)u = (union foo) x == u.i = x;
)__FUNCTION__
)int a[2]={p - q,p + q}
)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ší.
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:
g
- cokoliv (konstanta, registr, paměť)r
- libovolná hodnota v registrum
- hodnota musí být v pamětii
- hodnota musí být ,,immediate'' tedy konstanta známá při
kompilacia
- eax, d
- edx, c
- ecx, d
- edx, D
- edi, S
- esiq
- a
, b
, c
nebo
d
f
- floating point registrt
- první fp registr (top of stack)s
- druhý fp registr (second)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:
k
- celé slovo (eax)b
- byte (al)h
- horní byta (ah)w
- word (ax)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:
1:
...
jne 1b
tedy aby assembler věděl, že se odkazujete na nejbližší návěští
jménem 1 dozadu (nebo 1f pro dopředu)"memory"
-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
a __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?
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.
__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é.
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.
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.
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).
Dotazy a připomínky ohledně stránky posílejte na hubicka@paru.cas.cz