Come altri hanno detto, se non significa nulla a partire da C ++ 14 , quindi consideriamo l' __restrict__
estensione GCC che fa lo stesso del C99 restrict
.
C99
restrict
afferma che due puntatori non possono puntare a aree di memoria sovrapposte. L'uso più comune è per gli argomenti delle funzioni.
Ciò limita il modo in cui è possibile chiamare la funzione, ma consente ulteriori ottimizzazioni di compilazione.
Se il chiamante non segue il restrict
contratto, comportamento indefinito.
Il progetto C99 N1256 6.7.3 / 7 "Qualificatori di tipo" dice:
L'uso previsto del qualificatore di limitazione (come la classe di archiviazione dei registri) è quello di promuovere l'ottimizzazione e l'eliminazione di tutte le istanze del qualificatore da tutte le unità di traduzione di preelaborazione che compongono un programma conforme non ne modifica il significato (vale a dire comportamento osservabile).
e 6.7.3.1 "Definizione formale di restringimento" fornisce i dettagli cruenti.
Una possibile ottimizzazione
L' esempio di Wikipedia è molto illuminante.
Mostra chiaramente come consente di salvare un'istruzione di assemblaggio .
Senza restrizioni:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo assemblaggio:
load R1 ← *x ; Load the value of x pointer
load R2 ← *a ; Load the value of a pointer
add R2 += R1 ; Perform Addition
set R2 → *a ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus
; the value of x will change when the value of a
; changes.
load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b
Con limitazione:
void fr(int *restrict a, int *restrict b, int *restrict x);
Pseudo assemblaggio:
load R1 ← *x
load R2 ← *a
add R2 += R1
set R2 → *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2 ← *b
add R2 += R1
set R2 → *b
GCC lo fa davvero?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
objdump -S main.o
Con -O0
, sono gli stessi.
Con -O3
:
void f(int *a, int *b, int *x) {
*a += *x;
0: 8b 02 mov (%rdx),%eax
2: 01 07 add %eax,(%rdi)
*b += *x;
4: 8b 02 mov (%rdx),%eax
6: 01 06 add %eax,(%rsi)
void fr(int *__restrict__ a, int *__restrict__ b, int *__restrict__ x) {
*a += *x;
10: 8b 02 mov (%rdx),%eax
12: 01 07 add %eax,(%rdi)
*b += *x;
14: 01 06 add %eax,(%rsi)
Per i non iniziati, la convenzione di chiamata è:
rdi
= primo parametro
rsi
= secondo parametro
rdx
= terzo parametro
L'output di GCC era persino più chiaro dell'articolo wiki: 4 istruzioni contro 3 istruzioni.
Array
Finora abbiamo risparmi con una singola istruzione, ma se il puntatore rappresenta array da sottoporre a loop, un caso d'uso comune, allora un gruppo di istruzioni potrebbe essere salvato, come menzionato da supercat e michael .
Considera ad esempio:
void f(char *restrict p1, char *restrict p2, size_t size) {
for (size_t i = 0; i < size; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
A causa di restrict
, un compilatore intelligente (o umano), potrebbe ottimizzarlo per:
memset(p1, 4, size);
memset(p2, 9, size);
Che è potenzialmente molto più efficiente in quanto può essere ottimizzato per l'assemblaggio su un'implementazione decente di libc (come glibc) È meglio usare std :: memcpy () o std :: copy () in termini di prestazioni? , possibilmente con le istruzioni SIMD .
Senza, limitare, questa ottimizzazione non potrebbe essere eseguita, ad esempio considerare:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Quindi la for
versione rende:
p1 == {4, 4, 4, 9}
mentre la memset
versione rende:
p1 == {4, 9, 9, 9}
GCC lo fa davvero?
GCC 5.2.1. Linux x86-64 Ubuntu 15.10:
gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o
Con -O0
, entrambi sono uguali.
Con -O3
:
con limitazione:
3f0: 48 85 d2 test %rdx,%rdx
3f3: 74 33 je 428 <fr+0x38>
3f5: 55 push %rbp
3f6: 53 push %rbx
3f7: 48 89 f5 mov %rsi,%rbp
3fa: be 04 00 00 00 mov $0x4,%esi
3ff: 48 89 d3 mov %rdx,%rbx
402: 48 83 ec 08 sub $0x8,%rsp
406: e8 00 00 00 00 callq 40b <fr+0x1b>
407: R_X86_64_PC32 memset-0x4
40b: 48 83 c4 08 add $0x8,%rsp
40f: 48 89 da mov %rbx,%rdx
412: 48 89 ef mov %rbp,%rdi
415: 5b pop %rbx
416: 5d pop %rbp
417: be 09 00 00 00 mov $0x9,%esi
41c: e9 00 00 00 00 jmpq 421 <fr+0x31>
41d: R_X86_64_PC32 memset-0x4
421: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
428: f3 c3 repz retq
Due memset
chiamate come previsto.
senza restrizioni: nessuna chiamata stdlib, solo un srotolamento a ciclo continuo di 16 iterazioni che non intendo riprodurre qui :-)
Non ho avuto la pazienza di confrontarli, ma credo che la versione con restrizioni sarà più veloce.
Regola aliasing rigorosa
La restrict
parola chiave influenza solo i puntatori di tipi compatibili (ad es. Due int*
) perché le regole di aliasing rigoroso affermano che l'alias di tipi incompatibili è un comportamento indefinito per impostazione predefinita, e quindi i compilatori possono presumere che non accada e ottimizzare via.
Vedi: Qual è la regola di aliasing rigorosa?
Funziona per i riferimenti?
Secondo i documenti GCC: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html con sintassi:
int &__restrict__ rref
Esiste anche una versione per le this
funzioni membro:
void T::fn () __restrict__
restrict
è una parola chiave c99. Sì, Rpbert S. Barnes, so che la maggior parte dei compilatori supporta__restrict__
. Noterai che qualsiasi cosa con doppio trattino basso è, per definizione, specifica dell'implementazione e quindi NON C ++ , ma una versione specifica del compilatore.