web-dev-qa-db-fra.com

Utilisation réaliste du mot clé "restreindre" C99?

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?

171
user90052

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 .

167
Michael

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ètre
  • rsi = second paramètre
  • rdx = troisième paramètre

La 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