Je feuilletais 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 fondamentalement une promesse du programmeur que le pointeur ne sera pas utilisé pour pointer ailleurs.
Quelqu'un peut-il proposer des cas réalistes où cela vaut la peine de l'utiliser?
c
gcc
c99
restrict-qualifier
user90052
la source
la source
memcpy
vsmemmove
est un exemple canonique.restrict
arguments de qualification pourmemcpy
permettent en principe d'optimiser agressivement une implémentation naïve, et 2) le simple appelmemcpy
permet au compilateur de supposer que les arguments qui lui sont donnés ne sont pas alias, ce qui pourrait permettre une certaine optimisation autour de l'memcpy
appel.memcpy(anything, anything, 0);
tant que no-op et assurez -vous que sip
est un pointeur sur au moinsn
octets inscriptibles,memcpy(p,p,n)
; n'aura aucun effet secondaire indésirable. De tels cas peuvent survenir ...Réponses:
restrict
dit que le pointeur est la seule chose qui accède à l'objet sous-jacent. Il élimine le potentiel 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 qui peuvent 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 sesrc2
chevauchent, ce qui signifie qu'il doit effectuer une multiplication à la fois, du début à la fin. En ayantrestrict
, 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 .la source
dest
un des vecteurs sources se chevauchent. Pourquoi y aurait-il un problème sisrc1
et sesrc2
chevauchaient?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ètrersi
= deuxième paramètrerdx
= troisième paramètreLa 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:
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 deuxint*
) 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
restrict
, mais GCC en a__restrict__
pour extension: que signifie le mot clé restrict en C ++?__attribute__((malloc))
, qui dit que la valeur de retour d'une fonction n'est aliasée sur rien: GCC: __attribute __ ((malloc))la source
void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }
, les qualificatifs restrict permettraient au compilateur de réécrire le code comme "memset (p1,4,50); memset (p2,9,50);". Restreindre est largement supérieur à l'alias basé sur le type; c'est dommage que les compilateurs se concentrent davantage sur ce dernier.__restrict
. Sinon, les doubles soulignements peuvent être mal interprétés comme une indication que vous criez.__restrict__
n'a plus de sens, car le compilateur optimisera la même chose. Cela me rappelle leregister
mot - clé. Toujours pour la réponse épique. Bon travail!