A C nyelvben egyetlen egy olyan kulcsszó van, ami nem található meg a C++ nyelvben, ez pedig a restrict. Ez a kulcsszó egyfajta Ãgéret a fordÃtónak, mégpedig arra, hogy az ilyen módon megjelölt mutatón keresztül elérhetÅ‘ adathoz más nem férhet hozzá (olvasásra és Ãrása sem), csak a megjelölt mutatón keresztül.
Ennek az az értelme, hogy Ãgy a fordÃtó több optimalizációt tud végrehajtani a redundáns olvasások eltávolÃtásával, ami gyorsabb kódot eredményezhet.
Nézzünk egy egyszerű példát:
void add_sub(int *x, int *y, int *amount)
{
*x += *amount;
*y -= *amount;
}
Ez a metódus az alábbi kódot fogja generálni a GCC -O3 kapcsolóval fordÃtva:
mov eax, DWORD PTR [rdx]
add DWORD PTR [rdi], eax
mov eax, DWORD PTR [rdx]
sub DWORD PTR [rsi], eax
ret
GCC esetén -O1, -O2 és -O3 opciók egyikével határozhatjuk meg a fordÃtás során az alkalmazandó optimalizációk mértékét. A -O3 kapcsoló arra utasÃtja a fordÃtót, hogy a legmagasabb szintű optimalizálást végezze el a kódon.
Az optimalizálás célja, hogy a lefordÃtott program gyorsabb és hatékonyabb legyen, még akkor is, ha ez több idÅ‘t vesz igénybe a fordÃtás során, illetve ha ez a lefordÃtott bináris méretének növekedésével jár.
Mivel az amount változó egy mutató, ezért minden műveletvégzés elÅ‘tt kiolvassa az értékét a fordÃtó a mellékhatások elkerülése érdekében. Ez azért történik Ãgy, mert az amount az lehet volatile is, amit vagy egy másik szál vagy hardver módosÃthat, ezért az értékének felhasználása elÅ‘tt biztos, ami biztos kiolvassa azt. Ez a kódban az add és sub műveletek elÅ‘tt megjelenÅ‘ mov formájában nyilvánul meg.
Nézzük mi változik, ha az amount változót restrict kulcsszóval látjuk el:
void add_subRestrict(int *x, int *y, int *restrict amount)
{
*x += *amount;
*y -= *amount;
}
Ez az alábbi kódot eredményezi:
mov eax, DWORD PTR [rdx]
add DWORD PTR [rdi], eax
sub DWORD PTR [rsi], eax
ret
Itt láthatóan csak egyszer kerül kiolvasásra az amount változó értéke, mivel a restrict-el azt mondtuk a fordÃtónak, hogy az amount által mutatott memóriaterületet más kódrészlet nem olvassa és Ãrja a függvény végrehajtása alatt.
A restrict valódi előnye akkor mutatkozik meg, amikor vektorműveleteket végzünk. Az alábbi példa egy általános vektor összeadást mutat be:
void vector_add(int64_t* dst, const int64_t *src1, const int64_t *src2, size_t n)
{
for (size_t i = 0; i < n; i++)
dst[i] = src1[i] + src2[i];
}
-O3 optimalizáció mellett az alábbi kódot generálja a GCC:
mov r8, rsi
.LBB3:
test rcx, rcx
je .L1
lea rax, [rcx-1]
cmp rax, 1
jbe .L9
lea rax, [rdx+8]
cmp rdi, rax
je .L9
lea rax, [rsi+8]
cmp rdi, rax
je .L9
mov rsi, rcx
.LVL1:
xor eax, eax
shr rsi
sal rsi, 4
.LVL2:
.L4:
movdqu xmm0, XMMWORD PTR [r8+rax]
movdqu xmm1, XMMWORD PTR [rdx+rax]
paddq xmm0, xmm1
movups XMMWORD PTR [rdi+rax], xmm0
add rax, 16
cmp rax, rsi
jne .L4
test cl, 1
je .L1
.LVL3:
and rcx, -2
.LVL4:
mov rax, QWORD PTR [rdx+rcx*8]
add rax, QWORD PTR [r8+rcx*8]
mov QWORD PTR [rdi+rcx*8], rax
.LVL5:
ret
.LVL6:
.L9:
xor eax, eax
.LVL7:
.L6:
mov rsi, QWORD PTR [rdx+rax*8]
add rsi, QWORD PTR [r8+rax*8]
mov QWORD PTR [rdi+rax*8], rsi
add rax, 1
.LVL8:
cmp rcx, rax
jne .L6
.LVL9:
.L1:
.LBE3:
ret
Mint látható, a generált kód elég komplex, elég sok memóriamozgatással (9db) és különböző ellenőrzésekkel.
Ezek az ellenÅ‘rzések azért szükségesek a kódban, mert a fordÃtó nem lehet biztos benne, hogy a dst, src és src2 által mutatott memóriaterületek között nincs átfedés. Ezért a mellékhatások elkerülése végett extra ellenÅ‘rzések generálódnak.
Nézzük meg, mi történik akkor, ha a dst változót, amit Ãrunk, megjelöljük arestrict kulcsszóval.
void vector_addRestrict(int64_t* restrict dst, const int64_t *src1, const int64_t *src2, size_t n)
{
for (size_t i = 0; i < n; i++)
dst[i] = src1[i] + src2[i];
}
A generálódó kód:
test rcx, rcx
je .L1
cmp rcx, 1
je .L6
mov r8, rcx
xor eax, eax
shr r8
sal r8, 4
.LVL1:
.L4:
movdqu xmm0, XMMWORD PTR [rsi+rax]
movdqu xmm1, XMMWORD PTR [rdx+rax]
paddq xmm0, xmm1
movups XMMWORD PTR [rdi+rax], xmm0
add rax, 16
cmp rax, r8
jne .L4
test cl, 1
je .L1
and rcx, -2
.LVL2:
.L3:
mov rax, QWORD PTR [rsi+rcx*8]
add rax, QWORD PTR [rdx+rcx*8]
mov QWORD PTR [rdi+rcx*8], rax
.LVL3:
.L1:
.LBE2:
ret
Ami elsÅ‘re szembetűnÅ‘ különbség, hogy jóval kevesebb kód generálódott, mivel a fordÃtó ebben az esetben független változónak tekinti a dst paraméter memóriaterületét. Ez pedig végsÅ‘ soron gyorsabb kódot eredményez.
A restrict problémái
A restrict kulcsszó használata egy kétélű kard, amivel nagyon könnyen megvághatjuk magunkat. Tény, hogy optimalizációs előnyökkel járhat a használata, de egy rosszul megjelölt paraméter és átfedésben lévő memóriaterületek esetén csúnya mellékhatásokat generál a kódunkba, amit nem lesz egyszerű megtalálni.
Használatának másik fÅ‘ problémája az, hogy a C++ fordÃtók nem támogatják a kulcsszó használatát. Ennek oka abban keresendÅ‘, hogy a restrict alapvetÅ‘en nem objektumorientált használatra lett kitalálva, ezért számos C++ szolgáltatással ütközne.
A restrict kulcsszót a C99 szabvány vezette be és használatának értelme/haszna beágyazott rendszerek és operációs rendszerek kritikus kódrészletei esetén mutatkozik meg igazán.