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.