Utilisation réaliste du mot-clé C99 'restrict'?

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 offrir des cas réalistes où il vaut la peine d'utiliser cela?

149
demandé sur Mat 2009-04-14 04:09:53

2 réponses

restrict dit que le pointeur est la seule chose qui accède à l'objet sous-jacent. Il élimine le potentiel d'aliasing 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 les vecteurs de nombres en mémoire, et 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 se chevauchent, ce qui signifie qu'il doit faire une multiplication à la fois, dès le début jusqu'à la fin. En ayant restrict, le compilateur est libre d'optimiser ce code en utilisant les instructions vectorielles.

, Wikipédia a une entrée sur restrict, avec un autre exemple, ici.

156
répondu Michael 2018-05-31 10:23:39

Le Wikipédia exemple est très éclairante.

, Il montre clairement comment , il permet d'enregistrer l'une des instructions de montage.

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 a may be equal to x.
load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b

, Avec de restreindre l':

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
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 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 GCC était encore plus claire que l'article wiki: 4 instructions vs 3 instructions.

Les Matrices

Jusqu'à présent, nous avons des économies d'instructions simples, mais si le pointeur représente des tableaux à boucler, un cas d'utilisation commun, alors un tas d'instructions pourraient être sauvegardées, 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);

Qui est potentiellement beaucoup plus efficace car il peut être optimisé pour un assemblage sur une implémentation libc décente (comme glibc): est-il préférable d'utiliser std::memcpy() ou std::copy() en termes de performance?

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 les mêmes.

Avec -O3:

  • , Avec de restreindre l':

    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 restreindre: pas d'appels stdlib, juste une boucle de 16 itérations déroulement que je n'ai pas l'intention de reproduire ici :-)

Je n'ai pas eu la patience de les comparer, mais je crois que la version restrict sera plus rapide.

C99

Regardons la norme pour l'exhaustivité.

restrict indique que deux pointeurs ne peuvent pas pointer vers des régions de mémoire qui se chevauchent. L'utilisation la plus courante est pour les arguments de fonction.

Cela limite la façon dont la fonction peut être appelée, mais permet plus d'optimisations à la compilation.

Si l'appelant ne suit pas le contrat restrict, comportement indéfini.

LeC99 n1256 projet 6.7.3/7 "Type les qualificatifs" dit:

L'utilisation prévue du qualificatif restrict (comme la classe de stockage register) 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 (comportement observable).

Et 6.7.3.1 "définition Formelle de restreindre" donne les détails sanglants.

Règle d'aliasing stricte

Le mot clé restrict affecte uniquement pointeurs de types compatibles (par exemple deux int*) parce que les règles d'aliasing strictes indiquent que l'aliasing des types incompatibles est un comportement indéfini par défaut, et donc les compilateurs peuvent supposer que cela ne se produit pas et optimiser loin.

Voir: Quelle est la règle d'aliasing stricte?

Voir aussi

109