L' exemple de Wikipedia est très éclairant.
Il montre clairement comment il permet de sauvegarder une instruction d'assemblage .
Sans restriction:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo assemblage:
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
Avec restrict:
void fr(int *restrict a, int *restrict b, int *restrict x);
Pseudo assemblage:
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
Est-ce que GCC le fait vraiment?
GCC 4.8 Linux x86-64:
gcc -g -std=c99 -O0 -c main.c
objdump -S main.o
Avec -O0
, ce sont les mêmes.
Avec -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)
Pour les non-initiés, la convention d'appel est:
rdi
= premier paramètre
rsi
= deuxième paramètre
rdx
= troisième paramètre
La sortie de GCC était encore plus claire que l'article du wiki: 4 instructions contre 3 instructions.
Tableaux
Jusqu'à présent, nous avons des économies d'instructions uniques, mais si le pointeur représente des tableaux à boucler, un cas d'utilisation courant, alors un tas d'instructions pourrait être enregistré, comme mentionné par supercat .
Considérez par exemple:
void f(char *restrict p1, char *restrict p2) {
for (int i = 0; i < 50; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
En raison de restrict
, un compilateur intelligent (ou humain) pourrait optimiser cela pour:
memset(p1, 4, 50);
memset(p2, 9, 50);
ce qui est potentiellement beaucoup plus efficace car il peut être optimisé pour l'assemblage sur une implémentation libc décente (comme la glibc): Est-il préférable d'utiliser std :: memcpy () ou std :: copy () en termes de performances?
Est-ce que GCC le fait vraiment?
GCC 5.2.1.Linux x86-64 Ubuntu 15.10:
gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o
Avec -O0
, les deux sont identiques.
Avec -O3
:
avec restreindre:
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
Deux memset
appels comme prévu.
sans restriction: pas d'appels stdlib, juste une boucle large de 16 itérations que je n'ai pas l'intention de reproduire ici :-)
Je n'ai pas eu la patience de les comparer, mais je pense que la version restrictive sera plus rapide.
C99
Examinons la norme par souci d'exhaustivité.
restrict
dit que deux pointeurs ne peuvent pas pointer vers des régions de mémoire qui se chevauchent. L'utilisation la plus courante concerne les arguments de fonction.
Cela limite la façon dont la fonction peut être appelée, mais permet davantage d'optimisations au moment de la compilation.
Si l'appelant ne respecte pas le restrict
contrat, comportement indéfini.
Le projet de C99 N1256 6.7.3 / 7 "Qualificatifs de type" dit:
L'utilisation prévue du qualificatif restrict (comme la classe de stockage de registre) est de promouvoir l'optimisation, et la suppression de toutes les instances du qualificatif de toutes les unités de traduction de prétraitement composant un programme conforme ne change pas sa signification (c'est-à-dire le comportement observable).
et 6.7.3.1 "Définition formelle de restreindre" donne les détails sanglants.
Règle d'aliasing stricte
Le restrict
mot-clé n'affecte que les pointeurs de types compatibles (par exemple deux int*
) car les règles strictes d'aliasing indiquent que l'aliasing des types incompatibles est un comportement indéfini par défaut, et les compilateurs peuvent donc supposer que cela ne se produit pas et s'optimiser.
Voir: Quelle est la règle stricte d'aliasing?
Voir également
memcpy
vsmemmove
est un exemple canonique.