Maison  >  Article  >  développement back-end  >  Le mot-clé obscur « restreindre » en C

Le mot-clé obscur « restreindre » en C

WBOY
WBOYoriginal
2024-09-11 06:36:02379parcourir

The Obscure “restrict” Keyword in C

Introduction

Entre autres choses, C99 a ajouté le mot-clé restrict comme moyen pour un programmeur de spécifier qu'un pointeur est le seul pointeur vers un objet donné dans une portée et, par conséquent, de donner au compilateur un « indice » " qu'il peut effectuer des optimisations supplémentaires lors de l'accès à l'objet via ce pointeur.

Le problème

Pour illustrer le problème que restrict était censé résoudre, considérons une fonction comme :

void update_ptrs( int *p, int *q, int const *v ) {
  *p += *v;
  *q += *v;
}

pour lequel le compilateur générera du code x86-64 comme :

mov eax, [rdx]  ; tmp = *v   // 1
add [rdi], eax  ; *p += tmp
mov eax, [rdx]  ; tmp = *v   // 3
add [rsi], eax  ; *q += tmp

Vous vous demandez peut-être pourquoi il génère la ligne 3 puisqu'elle semble redondante avec la ligne 1. Le problème est que le compilateur ne peut pas savoir que vous n'avez pas fait quelque chose comme ceci :

int x = 1, v = 2;
update_ptrs( &v, &x, &v );   // x = 5, v = 4

Dans update_ptrs(), p et v alias le même int, donc le compilateur doit jouer la sécurité et supposer que la valeur de *v peut changer entre les lectures, d'où l'instruction mov supplémentaire.

En général, les pointeurs en C confondent l'optimisation puisque le compilateur ne peut pas savoir si deux pointeurs s'alias l'un l'autre. Dans le code critique en termes de performances, l'élimination des lectures de mémoire pourrait être une énorme victoire si le compilateur pouvait le faire en toute sécurité.

La solution

Pour résoudre le problème susmentionné, restrict a été ajouté à C pour vous permettre de spécifier qu'un pointeur donné est le seul pointeur vers un objet dans la portée du pointeur, c'est-à-dire aucun autre pointeur dans les mêmes alias de portée ça.

Pour utiliser restrict, vous l'insérez entre le * et le nom du pointeur dans une déclaration. Un update_ptrs() réécrit pour utiliser restrict serait :

void update_ptrs_v2( int *restrict p, int *restrict q,
                     int const *restrict v ) {
  *p += *v;
  *q += *v;
}

(Lisez de droite à gauche, par exemple, v est un pointeur restreint vers un entier constant ; ou utilisez cdecl.)

En ajoutant restrict, le compilateur peut désormais générer du code comme :

mov eax, [rdx]  ; tmp = *v
add [rdi], eax  ; *p += tmp
add [rsi], eax  ; *q += tmp

Maintenant, le compilateur a pu éluder la ligne 3 précédente de l'instruction mov supplémentaire.

L'exemple le plus connu d'utilisation de restrict est peut-être la fonction de bibliothèque standard memcpy(). C'est le moyen le plus rapide de copier une partie de la mémoire si les adresses source et destination ne ne se chevauchent. La fonction memmove() légèrement plus lente existe pour être utilisée lorsque les adresses do se chevauchent.

Pièges

Une mauvaise utilisation de restrict entraîne un comportement non défini, par exemple en passant des pointeurs qui font des alias les uns les autres vers update_ptrs_v2() ou memcpy(). Dans certains cas, le compilateur peut vous avertir, mais pas dans tous les cas, alors ne comptez pas sur le compilateur pour détecter les utilisations abusives.

Notez que la restriction s'applique à une portée donnée. L'attribution d'un pointeur restreint à un autre dans la même portée entraîne un comportement indéfini :

void f( int *restrict d, int *restrict s ) {
  int *restrict p = s;    // undefined behavior

Cependant, vous pouvez très bien attribuer un pointeur restreint à un pointeur non restreint :

void f( int *restrict d, int *restrict s ) {
  int *p = s;             // OK

Même si p n'est pas restreint, le compilateur peut toujours effectuer les mêmes optimisations.

Il est également possible d'attribuer un pointeur restreint dans une portée interne à un autre dans une portée externe (mais pas l'inverse) :

void f( int *restrict d, int *restrict s ) {
  {                       // inner scope
    int *restrict p = s;  // OK
    // ...
    s = p;                // undefined behavior
  }
}

Quand (et quand ne pas) utiliser la restriction

Tout d'abord, vous devez absolument profiler votre code (et peut-être même regarder le code assembleur généré) pour voir si l'utilisation de restrict apporte réellement une amélioration significative des performances pour justifier le risque de pièges potentiels. Diagnostiquer les bugs causés par une mauvaise utilisation de restrict est très difficile à faire.

Deuxièmement, si l'utilisation de restrict est limitée à l'implémentation d'une fonction où la mémoire accessible via des pointeurs restreints a été allouée par vous, alors c'est plus sûr. Par exemple, étant donné :

void safer( unsigned n ) {
  n += n % 2 != 0;  // make even by rounding up
  int *const array = malloc( n * sizeof(unsigned) );
  unsigned *restrict half_1st = array;
  unsigned *restrict half_2nd = array + n/2;
  // ...
  free( array );
}

le code pourrait fonctionner sur les première et deuxième moitiés du tableau en toute sécurité car elles ne se chevauchent pas (en supposant que vous n'accédiez jamais à half_1st[n/2] ou au-delà).

Troisièmement, si restrict est utilisé dans les paramètres d'une fonction, alors c'est potentiellement moins sûr. Par exemple, contrastez safer() avec update_ptrs_v2() où l'appelant contrôle les pointeurs. Pour tout ce que vous savez, l'appelant s'est trompé et a transmis des pointeurs qui alias.

Divers

Seuls les pointeurs vers des objets (ou vides) peuvent être qualifiés avec restrict :

restrict int x;       // error: can't restrict object
int restrict *p;      // error: pointer to restrict object
int (*restrict f)();  // error: pointer-to-function

Vous pouvez utiliser restrict pour les membres de la structure, par exemple :

struct node {
   void *restrict data;
   struct node *restrict left;
   struct node *restrict right;
};

dit que data sera le seul pointeur vers ces données et que gauche et droite ne pointeront jamais vers le même nœud. Cependant, l'utilisation de restrict pour les membres d'une structure est très rare.

Enfin, C++ n'a pas de restriction. Pourquoi pas? Il y a une longue réponse, mais la version TL;DR est la suivante :

  • Cela peut être une source de bugs difficiles à trouver que le comité C++ n'a pas voulu importer depuis C.
  • L'utilisation accrue de pointeurs par C++, par exemple this, rend l'utilisation de restrictions en toute sécurité encore plus difficile.

Cependant, de nombreux compilateurs ont __restrict__ comme extension.

Conclusion

Dans des cas limités, l'utilisation de restrict peut conduire à des améliorations de performances, mais il existe plusieurs pièges importants. Si vous envisagez d'utiliser la restriction, profilez d'abord votre code.

Utilisez judicieusement.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn