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?
GCC 4.8 Linux x86-64:
gcc -g -std=c99 -O0 -c main.c
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 è stato 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 .
Considera ad esempio:
void f(char *restrict p1, char *restrict p2) {
for (int i = 0; i < 50; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
A causa di restrict
, un compilatore intelligente (o umano), potrebbe ottimizzarlo per:
memset(p1, 4, 50);
memset(p2, 9, 50);
che è potenzialmente molto più efficiente in quanto può essere assemblato ottimizzato su un'implementazione libc decente (come glibc): è meglio usare std :: memcpy () o std :: copy () in termini di prestazioni?
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.
C99
Diamo un'occhiata allo standard per completezza.
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 in fase 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.
Regola aliasing rigorosa
La restrict
parola chiave influenza solo i puntatori di tipi compatibili (ad esempio due int*
) perché le regole di aliasing rigoroso indicano 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?
Guarda anche
memcpy
vsmemmove
è un esempio canonico.