J'essaie de comprendre quand et quand ne pas utiliser le mot clé restrict
en C et dans quelles situations il procure un avantage tangible.
Après avoir lu " Démystifier le mot-clé de restriction ", (qui fournit quelques règles de base sur l'utilisation), j'ai l'impression que lorsqu'une fonction est transmise à des pointeurs, elle doit prendre en compte la possibilité que les données pointées puissent chevauchement (alias) avec tous les autres arguments passés dans la fonction. Étant donné une fonction:
foo(int *a, int *b, int *c, int n) {
for (int i = 0; i<n; ++i) {
b[i] = b[i] + c[i];
a[i] = a[i] + b[i] * c[i];
}
}
le compilateur doit recharger c
dans la deuxième expression, parce que peut-être b
et c
désignent le même emplacement. Il doit également attendre que b
soit stocké avant de pouvoir charger a
pour la même raison. Il doit alors attendre que a
soit stocké et doit recharger b
et c
au début de la boucle suivante. Si vous appelez la fonction comme ceci:
int a[N];
foo(a, a, a, N);
alors vous pouvez voir pourquoi le compilateur doit faire cela. L'utilisation de restrict
indique effectivement au compilateur que vous ne ferez jamais cela, afin qu'il puisse abandonner la charge redondante de c
et charger a
avant que b
ne soit stocké.
Jusqu'ici, j'ai compris qu'il est judicieux d'utiliser restrict
sur les pointeurs que vous passez à des fonctions qui ne seront pas en ligne. Apparemment, si le code est en ligne, le compilateur peut comprendre que les pointeurs ne se chevauchent pas.
Maintenant, voici où les choses commencent à devenir floues pour moi.
Dans le document d'Ulrich Drepper, " Ce que tout programmeur devrait savoir sur la mémoire " il déclare que "sauf si restrict est utilisé, tous les accès de pointeur sont des sources potentielles d'aliasing", et il donne un exemple de code spécifique d'une sous-matrice multiplier la matrice où il utilise restrict
.
Cependant, lorsque je compile son exemple de code avec ou sans restrict
, je reçois des fichiers binaires identiques dans les deux cas. J'utilise gcc version 4.2.4 (Ubuntu 4.2.4-1ubuntu4)
Ce que je ne peux pas comprendre dans le code suivant, c'est s'il faut le réécrire pour utiliser plus largement restrict
, ou si l'analyse des alias dans GCC est tellement bonne qu'elle permet de comprendre qu'aucun des arguments alias L'un et l'autre. À des fins purement pédagogiques, comment puis-je utiliser ou non la question restrict
dans ce code - et pourquoi?
Pour restrict
compilé avec:
gcc -DCLS=$(getconf LEVEL1_DCACHE_LINESIZE) -DUSE_RESTRICT -Wextra -std=c99 -O3 matrixMul.c -o matrixMul
Supprimez simplement -DUSE_RESTRICT
pour ne pas utiliser restrict
.
#include <stdlib.h>
#include <stdio.h>
#include <emmintrin.h>
#ifdef USE_RESTRICT
#else
#define restrict
#endif
#define N 1000
double _res[N][N] __attribute__ ((aligned (64)));
double _mul1[N][N] __attribute__ ((aligned (64)))
= { [0 ... (N-1)]
= { [0 ... (N-1)] = 1.1f }};
double _mul2[N][N] __attribute__ ((aligned (64)))
= { [0 ... (N-1)]
= { [0 ... (N-1)] = 2.2f }};
#define SM (CLS / sizeof (double))
void mm(double (* restrict res)[N], double (* restrict mul1)[N],
double (* restrict mul2)[N]) __attribute__ ((noinline));
void mm(double (* restrict res)[N], double (* restrict mul1)[N],
double (* restrict mul2)[N])
{
int i, i2, j, j2, k, k2;
double *restrict rres;
double *restrict rmul1;
double *restrict rmul2;
for (i = 0; i < N; i += SM)
for (j = 0; j < N; j += SM)
for (k = 0; k < N; k += SM)
for (i2 = 0, rres = &res[i][j],
rmul1 = &mul1[i][k]; i2 < SM;
++i2, rres += N, rmul1 += N)
for (k2 = 0, rmul2 = &mul2[k][j];
k2 < SM; ++k2, rmul2 += N)
for (j2 = 0; j2 < SM; ++j2)
rres[j2] += rmul1[k2] * rmul2[j2];
}
int main (void)
{
mm(_res, _mul1, _mul2);
return 0;
}
De plus, GCC 4.0.0-4.4 a un bogue de régression qui fait que le mot clé restrict est ignoré. Ce bogue a été signalé comme corrigé dans la version 4.5 (j'ai cependant perdu le numéro du bogue).
C'est un indice pour l'optimiseur de code. L'utilisation de restrict garantit qu'il peut stocker une variable de pointeur dans un registre de la CPU et ne pas avoir à vider une mise à jour de la valeur du pointeur en mémoire, de sorte qu'un alias soit également mis à jour.
Qu'il en profite ou non dépend fortement des détails de mise en œuvre de l'optimiseur et du processeur. Les optimiseurs de code sont déjà fortement investis dans la détection du non-aliasing, une optimisation si importante. Cela ne devrait pas avoir de difficulté à détecter cela dans votre code.
(Je ne sais pas si l'utilisation de ce mot clé vous procure un avantage significatif. Il est très facile au programmeur de se tromper avec ce qualificatif car il n'y a pas de contrainte, donc un optimiseur ne peut pas être sûr que le programmeur ne "ment" pas. )
Lorsque vous savez qu'un pointeur A est le seul pointeur vers une région de la mémoire, c'est-à-dire qu'il n'a pas d'alias (c'est-à-dire que tout autre pointeur B sera nécessairement différent de A, B! = A), vous pouvez dire ce fait à l'optimiseur en qualifiant le type de A avec le mot clé "restrict".
J'ai écrit à ce sujet ici: http://mathdev.org/node/23 et j'ai essayé de montrer que certains pointeurs restreints sont en fait "linéaires" (comme mentionné dans ce post).
Il convient de noter que les versions récentes de clang
sont capables de générer du code avec une vérification à l'exécution pour le crénelage, et deux chemins de code: un pour les cas où il existe un crénelage potentiel et l'autre pour le cas où il est évident qu'il n'y a aucune chance qu'il en soit ainsi. .
Cela dépend clairement de l'étendue des données signalées par le compilateur, comme dans l'exemple ci-dessus.
Je crois que la justification principale est que les programmes utilisent beaucoup le format STL - et en particulier le <algorithm>
, où il est difficile ou impossible d’introduire le qualificatif __restrict
.
Bien sûr, tout cela se fait au détriment de la taille du code, mais cela supprime beaucoup de bugs cachés pouvant résulter pour les pointeurs déclarés comme __restrict
, qui ne se chevauchent pas autant que le développeur le pensait.
Je serais surpris que GCC n’ait pas eu cette optimisation.
Peut-être que l'optimisation effectuée ici ne repose pas sur le fait que les pointeurs ne sont pas aliasés? À moins que vous ne préchargiez plusieurs éléments mul2 avant d'écrire le résultat dans res2, je ne vois aucun problème d'aliasing.
Dans le premier morceau de code que vous montrez, il est assez clair quel type de problème de pseudonymes peut survenir . Ici, ce n'est pas si clair.
Relisant l'article de Dreppers, il ne dit pas spécifiquement que restreindre pourrait résoudre quoi que ce soit. Il y a même cette phrase:
{En théorie, le mot clé restrict introduit dans le langage C dans le La révision de 1999 devrait résoudre le problème. Les compilateurs n'ont pas rattrapé pourtant, cependant. La raison est principalement que trop de code incorrect existe qui induirait en erreur le compilateur et causerait pour générer un code objet incorrect.}
Dans ce code, des optimisations d'accès à la mémoire ont déjà été effectuées dans l'algorithme. L’optimisation résiduelle semble se faire dans le code vectorisé présenté en annexe. Donc pour le code présenté ici, je suppose qu'il n'y a pas de différence, car aucune optimisation basée sur restrict n'est effectuée. Chaque accès au pointeur est une source de repliement, mais toutes les optimisations ne reposent pas sur la détermination de la taille.
L’optimisation prématurée étant la racine de tous les maux, l’utilisation du mot-clé restrict devrait être limitée au cas que vous étudiez et optimisez activement, et non utilisée où qu’elle soit utilisée.
S'il y a une différence, déplacer mm
vers un DSO distinct (de sorte que gcc ne puisse plus tout savoir sur le code d'appel) sera le moyen de le démontrer.
Le problème avec votre exemple de code est que le compilateur insère simplement l'appel et voit qu'il n'y a pas d'alias possible dans votre exemple. Je vous suggère de supprimer la fonction main () et de la compiler avec -c.
Utilisez-vous Ubuntu 32 ou 64 bits? Si 32 bits, vous devez ajouter -march=core2 -mfpmath=sse
(ou quelle que soit l'architecture de votre processeur), sinon il n'utilise pas SSE. Deuxièmement, pour activer la vectorisation avec GCC 4.2, vous devez ajouter l'option -ftree-vectorize
(à partir de la version 4.3 ou 4.4, cette option est incluse par défaut dans -O3
). Il peut également être nécessaire d’ajouter -ffast-math
(ou une autre option fournissant une sémantique détendue en virgule flottante) afin de permettre au compilateur de réorganiser les opérations en virgule flottante.
Ajoutez également l’option -ftree-vectorizer-verbose=1
pour savoir s’il parvient ou non à vectoriser la boucle; c'est un moyen facile de vérifier l'effet de l'ajout du mot clé restrict.