72 votes

Quel ordre de boucles imbriquées pour itérer sur un tableau 2D est le plus efficace ?

Lequel des ordres suivants de boucles imbriquées pour itérer sur un tableau 2D est le plus efficace en termes de temps (performance du cache) ? Pourquoi ?

int a[100][100];

for(i=0; i<100; i++)
{
   for(j=0; j<100; j++)
   {
       a[i][j] = 10;    
   }
}

o

for(i=0; i<100; i++)
{
   for(j=0; j<100; j++)
   {
      a[j][i] = 10;    
   }
}

0 votes

Il doit y avoir une différence lors du stockage du tableau

1 votes

Vous les avez écrites. Maintenant, vous pouvez augmenter les chiffres et les tester par vous-même. Je parie qu'il n'y aura aucune différence due aux optimisations du compilateur, ou dans le pire des cas, la première sera plus rapide, car elle itère sur la zone contiguë de la mémoire.

6 votes

Petite note : utilisez "++i" au lieu de "i++". C'est plus rapide (bien que pour les nombres la différence avec "i++" soit très faible, pas comme pour les itérateurs STL).

64voto

MByD Points 78505

La première méthode est légèrement meilleure, car les cellules assignées sont placées les unes à côté des autres.

Première méthode :

[ ][ ][ ][ ][ ] ....
^1st assignment
   ^2nd assignment
[ ][ ][ ][ ][ ] ....
^101st assignment

Deuxième méthode :

[ ][ ][ ][ ][ ] ....
^1st assignment
   ^101st assignment
[ ][ ][ ][ ][ ] ....
^2nd assignment

0 votes

Vous voulez dire que leur accès est plus rapide que les autres ?

1 votes

Cela signifie qu'il y aura moins de ratés dans la mémoire cache et que le processeur sera capable de mieux deviner quelle mémoire sera accédée ensuite.

2 votes

Raymond Chen a publié un article similaire sur son blog, avec des photos et une bonne explication : blogs.msdn.com/b/oldnewthing/archive/2005/08/05/448073.aspx . Pensez à un bitmap comme à un tableau plus grand.

44voto

amit Points 74385
  1. Pour le tableau [100][100] - les deux sont identiques, si le cache L1 est plus grand, alors 100*100*sizeof(int) == 10000*sizeof(int) == [généralement] 40000. Remarque Sandy Bridge - 100*100 entiers devraient être suffisamment d'éléments pour voir une différence, puisque le cache L1 n'est que de 32k.

  2. Les compilateurs vont probablement optimiser ce code tout de même

  3. En supposant qu'il n'y ait pas d'optimisation du compilateur et que la matrice ne tienne pas dans le cache L1, le premier code est meilleur grâce aux performances du cache [généralement]. Chaque fois qu'un élément n'est pas trouvé dans le cache - vous obtenez un message d'erreur manque de cache - et doivent aller dans la RAM ou le cache L2 [qui sont beaucoup plus lents]. Le transfert d'éléments de la RAM au cache [remplissage du cache] se fait par blocs [généralement 8/16 octets] - ainsi, dans le premier code, vous obtenez au maximum taux d'échec de 1/4 [Dans le second code, les éléments qui étaient déjà dans le cache [insérés dans le remplissage du cache pour les éléments adjacents] - ont été retirés, et vous obtenez un manque de cache redondant.

    • Ceci est étroitement lié à la principe de localité qui est l'hypothèse générale utilisée lors de la mise en œuvre du système de cache. Le premier code suit ce principe alors que le second ne le fait pas - ainsi les performances du cache du premier seront meilleures que celles du second.

Conclusion : Pour toutes les implémentations de cache que je connais, la première ne sera pas pire que la seconde. Ils peuvent être identiques - s'il n'y a pas de cache du tout ou si tout le tableau rentre complètement dans le cache - ou en raison de l'optimisation du compilateur.

0 votes

K.... Cela signifie donc que le premier est toujours efficace... Nous pouvons accéder aux éléments beaucoup plus rapidement...

5 votes

@SachinMhetre : Pour toutes les implémentations de cache que je connais, la première ne sera pas pire que la seconde. Ils peuvent être identiques - s'il n'y a pas de cache du tout ou si tout le tableau tient dans le cache.

0 votes

Il est probablement utile de noter qu'il y a deux problèmes : le temps que prend la mémoire pour aller du cache L2 aux registres, et la bande passante entre le cache L2 et les registres. Si ce n'était qu'une question de latence, alors la préextraction (logicielle ou matérielle) pourrait éliminer la majeure partie de la différence entre les deux modes d'accès aux données. Cependant, la limite stricte ici est bande passante Puisque chaque accès à la mémoire lit une ligne entière de cache plutôt qu'un seul int, avec vos hypothèses, un modèle d'accès doit lire quatre fois plus de mémoire au total.

13voto

Luchian Grigore Points 136646

Ce type de micro-optimisation dépend de la plate-forme. Il vous faudra donc profiler le code pour pouvoir tirer une conclusion raisonnable.

22 votes

Je voterais pour si quelqu'un présentait une plateforme réelle où la première version était plus lente que la seconde. Oui, c'est de la micro-optimisation. Oui, cela ne fait probablement pas une différence notable. Non, vous ne devriez pas perdre votre temps à réécrire vos boucles à moins que le profileur n'indique qu'elles sont critiques en termes de performances. Mais si vous devez choisir entre deux façons également simples, claires et valables d'écrire un morceau de code, et vous connaissez une règle empirique qui dit que l'un des deux n'est pas plus lent que l'autre, alors pourquoi ne pas choisir celui qui n'est pas plus lent ?

2 votes

@IlmariKaronen J'ai voté pour votre commentaire. Mais notez que cela dépend au moins du langage. Fortran, par exemple, dispose le tableau en mémoire dans l'autre sens, donc pour Fortran, la première version sera probablement plus lente que la seconde.

10voto

Michael Foukarakis Points 14892

Dans votre deuxième extrait, la modification de l'élément j dans chaque itération produit un modèle avec une faible localité spatiale. Rappelez-vous que dans les coulisses, une référence de tableau calcule :

( ((y) * (row->width)) + (x) ) 

Considérons un cache L1 simplifié qui a suffisamment d'espace pour seulement 50 rangées de notre tableau. Pour les 50 premières itérations, vous paierez le coût inévitable de 50 manques de cache, mais que se passera-t-il ensuite ? Pour chaque itération de 50 à 99, vous manquerez encore de mémoire cache et devrez aller chercher dans la L2 (et/ou la RAM, etc.). Ensuite, x passe à 1 et y recommence, conduisant à un autre manquement au cache parce que la première ligne de votre tableau a été évincée du cache, et ainsi de suite.

Le premier extrait ne présente pas ce problème. Il accède au tableau dans ordre de rang-majeur Vous n'avez à payer qu'une seule fois pour les erreurs de cache (si une ligne de votre tableau n'est pas présente dans le cache au moment où la boucle commence) par ligne.

Ceci étant dit, cette question dépend beaucoup de l'architecture, il faut donc prendre en compte les spécificités (taille du cache L1, taille des lignes de cache, etc.) pour tirer une conclusion. ) pour tirer une conclusion. Vous devriez également mesurer dans les deux sens et suivre les événements matériels pour disposer de données concrètes permettant de tirer des conclusions.

6voto

Habib Points 93087

Si l'on considère que le C++ est le rang majeur, je pense que la première méthode sera un peu plus rapide. En mémoire, un tableau 2D est représenté dans un tableau à une seule dimension et les performances dépendent de l'accès à ce tableau, que ce soit par rangée ou par colonne.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X