Utilisation réaliste du mot-clé «restreindre» C99?

188

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?

user90052
la source
4
memcpyvs memmoveest un exemple canonique.
Alexandre C.
@AlexandreC .: Je ne pense pas que ce soit particulièrement applicable, car l'absence de qualificatif "restrict" n'implique pas que la logique du programme fonctionnera avec une surcharge source et destination, et la présence d'un tel qualificatif n'empêcherait pas une méthode appelée de déterminer si la source et la destination se chevauchent et, dans l'affirmative, remplacer dest par src + (dest-src) qui, puisqu'il est dérivé de src, serait autorisé à l'aliaser.
supercat
@supercat: C'est pourquoi je l'ai mis en commentaire. Cependant, 1) les restrictarguments de qualification pour memcpypermettent en principe d'optimiser agressivement une implémentation naïve, et 2) le simple appel memcpypermet 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' memcpyappel.
Alexandre C.
@AlexandreC .: Il serait très difficile pour un compilateur sur la plupart des plates-formes d'optimiser un memcpy naïf - même avec "restrict" - d'être presque aussi efficace qu'une version adaptée à la cible. Les optimisations côté appel ne nécessiteraient pas le mot-clé "restrict" et, dans certains cas, les efforts pour faciliter ceux-ci peuvent être contre-productifs. Par exemple, de nombreuses implémentations de memcpy pourraient, à zéro frais supplémentaires, ce qui concerne en memcpy(anything, anything, 0);tant que no-op et assurez -vous que si pest un pointeur sur au moins noctets inscriptibles, memcpy(p,p,n); n'aura aucun effet secondaire indésirable. De tels cas peuvent survenir ...
supercat
... naturellement dans certains types de code d'application (par exemple, une routine de tri échangeant un élément avec lui-même), et dans les implémentations où elles n'ont pas d'effets secondaires indésirables, laisser ces cas être traités par le code de cas général peut être plus efficace que d'avoir pour ajouter des tests de cas spéciaux. Malheureusement, certains rédacteurs de compilateurs semblent penser qu'il est préférable d'exiger que les programmeurs ajoutent du code que le compilateur ne pourra peut-être pas optimiser, afin de faciliter les «opportunités d'optimisation» que les compilateurs exploiteraient très rarement de toute façon.
supercat

Réponses:

186

restrictdit 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, src1et se src2chevauchent, 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 .

Michael
la source
3
@Michael - Si je ne me trompe pas, le problème serait uniquement lorsque l' destun des vecteurs sources se chevauchent. Pourquoi y aurait-il un problème si src1et se src2chevauchaient?
ysap
1
restrict n'a normalement d'effet que lorsqu'il pointe vers un objet qui est modifié, auquel cas il affirme qu'aucun effet secondaire caché ne doit être pris en compte. La plupart des compilateurs l'utilisent pour faciliter la vectorisation. Msvc utilise la vérification au moment de l'exécution pour le chevauchement des données à cette fin.
tim18
L'ajout du mot-clé register à la variable de boucle for le rend également plus rapide en plus d'ajouter restrict.
2
En fait, le mot-clé de registre est uniquement consultatif. Et dans les compilateurs depuis environ l'an 2000, le i (et le n pour la comparaison) de l'exemple seront optimisés dans un registre que vous utilisiez ou non le mot-clé register.
Mark Fischler
161

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 memsetappels 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é.

restrictdit 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 restrictcontrat, 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 restrictmot-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

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
11
Le qualificatif «restreindre» peut en fait permettre des économies beaucoup plus importantes. Par exemple, étant donné 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.
supercat
3
@ tim18: Le mot-clé "restrict" peut activer de nombreuses optimisations que même une optimisation basée sur le type agressive ne peut pas. De plus, l'existence de "restrict" dans le langage - contrairement à l'aliasing agressif basé sur le type - ne rend jamais impossible d'accomplir des tâches aussi efficacement que possible en leur absence (puisque le code qui serait cassé par "restrict" peut simplement ne pas l'utiliser, alors que le code qui est cassé par un TBAA agressif doit souvent être réécrit de manière moins efficace).
supercat
3
@ tim18: Entourez les éléments qui contiennent des doubles soulignements dans les backticks, comme dans __restrict. Sinon, les doubles soulignements peuvent être mal interprétés comme une indication que vous criez.
supercat le
3
Plus important que de ne pas crier, c'est que les traits de soulignement ont une signification directement pertinente par rapport au point que vous essayez de faire valoir.
remcycles
1
GCC 7 et Clang 9 - à n'importe quelle optimisation (-O1, -Os) __restrict__n'a plus de sens, car le compilateur optimisera la même chose. Cela me rappelle le registermot - clé. Toujours pour la réponse épique. Bon travail!
elcuco