J'étais toujours incertain, que signifie le mot clé restrict en C++?
Cela signifie-t-il que les deux pointeurs ou plus donnés à la fonction ne se chevauchent pas? Qu'est-ce que cela signifie?
Dans son article, Memory Optimization , Christer Ericson déclare que, bien que restrict
ne fasse pas encore partie de la norme C++, il est pris en charge par de nombreux compilateurs et il recommande son utilisation lorsque celle-ci est disponible:
mot clé restreint
! Nouveauté 1999 ANSI/ISO C
! Pas encore en standard C++, mais supporté par de nombreux compilateurs C++
! Un indice seulement, alors ne faites rien et restez conforme
Un pointeur qualifié (ou une référence) ...
! ... est fondamentalement une promesse faite au compilateur que, pour l'étendue du pointeur, la cible du pointeur ne sera accessible que par ce pointeur (et les pointeurs copiés à partir de celui-ci).
Dans les compilateurs C++ qui le supportent, il devrait probablement se comporter comme dans C.
Voir ceci SO post pour plus de détails: tilisation réaliste du mot clé ‘restrict’ de C99??
Prenez une demi-heure pour parcourir le papier d'Ericson, c'est intéressant et vaut la peine.
Modifier
J'ai également constaté que le compilateur d'IBM AIX C/C++ prend en charge le __restrict__
mot clé .
g ++ semble également supporter cela car le programme suivant compile proprement sur g ++:
#include <stdio.h>
int foo(int * __restrict__ a, int * __restrict__ b) {
return *a + *b;
}
int main(void) {
int a = 1, b = 1, c;
c = foo(&a, &b);
printf("c == %d\n", c);
return 0;
}
J'ai aussi trouvé un bel article sur l'utilisation de restrict
:
démystifier le mot clé Restrict
Edit2
J'ai rencontré un article qui traite spécifiquement de l'utilisation de restrict dans les programmes C++:
Load-hit-stores et le mot clé __restrict
Microsoft Visual C++ prend également en charge le __restrict
mot clé .
Comme d’autres l’ont dit, si signifie rien à partir de C++ 14 , considérons donc le __restrict__
Extension GCC qui fait la même chose que C99 restrict
.
C99
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 de 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 occurrences 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.
Une optimisation possible
Le exemple Wikipedia est très éclairant.
Il montre clairement comment, en tant que , 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?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
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, 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'à présent, 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 et michael .
Considérons par exemple:
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;
}
}
En raison de restrict
, un compilateur intelligent (ou humain) pourrait optimiser cela pour:
memset(p1, 4, size);
memset(p2, 9, size);
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? =, éventuellement avec instructions SIMD .
Sans, restreindre, cette optimisation ne pourrait pas être effectuée, par exemple. considérer:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Alors for
version fait:
p1 == {4, 4, 4, 9}
tandis que la version memset
fait:
p1 == {4, 9, 9, 9}
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.
Règle de pseudonyme stricte
Le mot clé restrict
n'affecte que les pointeurs de types compatibles (par exemple, deux int*
) parce que les règles strictes sur le crénelage indiquent que le crénelage des types incompatibles est un comportement indéfini par défaut, de sorte que les compilateurs peuvent supposer que cela ne se produit pas et l’optimiser.
Voir: Quelle est la règle de crénelage strict?
Est-ce que ça marche pour les références?
Selon la documentation de GCC, il fait: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html avec la syntaxe:
int &__restrict__ rref
Il existe même une version pour this
des fonctions membres:
void T::fn () __restrict__
Rien. Il a été ajouté à la norme C99.
Ce mot clé n'existe pas en C++. La liste des mots-clés C++ se trouve dans la section 2.11/1 du standard de langage C++. restrict
est un mot clé dans la version C99 du langage C et non dans C++.
Etant donné que les fichiers d’en-tête de certaines bibliothèques C utilisent le mot-clé, le langage C++ devra faire quelque chose à ce sujet .. au minimum, en ignorant le mot-clé, nous n’avons donc pas à # définir le mot-clé en une macro vierge pour le supprimer. .