Je parcourais de la documentation et des questions/réponses et je l'ai vu mentionné. J'ai lu une brève description, indiquant que ce serait essentiellement une promesse du programmeur que le pointeur ne sera pas utilisé pour pointer ailleurs.
Quelqu'un peut-il proposer des cas réalistes dans lesquels il vaut la peine de l'utiliser?
restrict
indique que le pointeur est la seule chose qui accède à l'objet sous-jacent. Cela élimine le risque d'alias de pointeur, permettant une meilleure optimisation par le compilateur.
Par exemple, supposons que j'ai une machine avec des instructions spécialisées pouvant multiplier des vecteurs de nombres en mémoire, et que j'ai le code suivant:
void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
for(int i = 0; i < n; i++)
{
dest[i] = src1[i]*src2[i];
}
}
Le compilateur doit gérer correctement si dest
, src1
, et src2
_ chevauchement, ce qui signifie qu’il doit effectuer une multiplication à la fois, du début à la fin. En ayant restrict
, le compilateur est libre d'optimiser ce code en utilisant les instructions vectorielles.
Wikipedia a une entrée sur restrict
, avec un autre exemple, ici .
Le exemple Wikipedia est très éclairant.
Il montre clairement comment il permet de sauvegarder une instruction d'assemblage .
Sans restreindre:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo Assemblée:
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 a may be equal to x.
load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b
Avec restreindre:
void fr(int *restrict a, int *restrict b, int *restrict x);
Pseudo Assemblée:
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
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
, Ils sont identiques.
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, le convention d'appel est:
rdi
= premier paramètrersi
= second paramètrerdx
= troisième paramètreLa sortie de GCC était encore plus claire que l’article du wiki: 4 instructions vs 3 instructions.
Tableaux
Jusqu'ici, nous avons des économies d'instruction simples, mais si le pointeur représente les tableaux à boucler, cas d'utilisation courant, un ensemble d'instructions pourrait être sauvegardé, comme mentionné par supercat .
Considérons 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é par Assembly sur une implémentation décente de libc (comme 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 appels memset
comme prévu.
sans restriction: pas d'appels stdlib, juste une itération de 16 déroulement de la boucle 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 restreinte sera plus rapide.
C99
Regardons la norme pour la complétude.
restrict
indique que deux pointeurs ne peuvent pas pointer sur des régions de mémoire qui se chevauchent. L'utilisation la plus courante est pour les arguments de fonction.
Cela restreint le mode d’appel de la fonction, mais permet davantage d’optimisations à la compilation.
Si l'appelant ne suit pas le contrat restrict
, comportement non défini.
Le brouillon C99 N1256 6.7.3/7 "Qualificatifs de type" indique:
L’utilisation prévue du qualificatif restrict (comme la classe de stockage de registre) est de promouvoir l’optimisation. La suppression de toutes les instances du qualificatif de toutes les unités de traduction en cours de prétraitement composant un programme conforme ne change pas sa signification (comportement observable, par exemple).
et 6.7.3.1 "Définition formelle de restreindre" donne les détails sanglants.
Règle de pseudonyme stricte
Le mot-clé restrict
n'affecte que les pointeurs de types compatibles (par exemple, deux int*
), Car les règles de crénelage strictes indiquent que le crénelage de types incompatibles est un comportement indéfini par défaut. Les compilateurs peuvent donc en déduire optimiser loin.
Voir: Quelle est la règle de crénelage strict?
Voir aussi
restrict
, mais GCC a l'extension __restrict__
: Que signifie le mot clé restrict en C++?__attribute__((malloc))
, qui indique que la valeur de retour d'une fonction n'est pas aliasé: GCC: __attribute __ ((malloc))