Comme d'autres l'ont dit, le problème est le stockage à l'emplacement de la mémoire dans le tableau : x[i][j]
. Voici un aperçu de la raison :
Vous avez un tableau à deux dimensions, mais la mémoire de l'ordinateur est par nature unidimensionnelle. Donc, alors que vous imaginez votre tableau comme ceci :
0,0 | 0,1 | 0,2 | 0,3
----+-----+-----+----
1,0 | 1,1 | 1,2 | 1,3
----+-----+-----+----
2,0 | 2,1 | 2,2 | 2,3
Votre ordinateur le stocke en mémoire sous la forme d'une seule ligne :
0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 | 2,0 | 2,1 | 2,2 | 2,3
Dans le deuxième exemple, vous accédez au tableau en bouclant d'abord sur le deuxième nombre, c'est-à-dire.. :
x[0][0]
x[0][1]
x[0][2]
x[0][3]
x[1][0] etc...
Ce qui signifie que vous les frappez tous dans l'ordre. Maintenant, regardez la 1ère version. Tu le fais :
x[0][0]
x[1][0]
x[2][0]
x[0][1]
x[1][1] etc...
En raison de la façon dont C a disposé le tableau à deux dimensions en mémoire, vous lui demandez de sauter partout. Mais maintenant, l'argument massue : Pourquoi est-ce important ? Tous les accès à la mémoire sont les mêmes, non ?
Non : à cause des caches. Les données de votre mémoire sont transmises à l'unité centrale par petits morceaux (appelés "lignes de cache"), généralement de 64 octets. Si vous avez des entiers de 4 octets, cela signifie que vous obtenez 16 entiers consécutifs dans un petit paquet bien ordonné. L'extraction de ces morceaux de mémoire est en fait assez lente ; votre CPU peut effectuer beaucoup de travail dans le temps nécessaire au chargement d'une seule ligne de cache.
Maintenant, regardez l'ordre des accès : Le deuxième exemple consiste à (1) saisir un morceau de 16 ints, (2) les modifier tous, (3) répéter 4000*4000/16 fois. C'est agréable et rapide, et le CPU a toujours quelque chose sur lequel travailler.
Le premier exemple est (1) de prendre un morceau de 16 ints, (2) de modifier un seul d'entre eux, (3) de répéter 4000*4000 fois. Cela va nécessiter 16 fois le nombre de "fetches" de la mémoire. Votre CPU devra en fait passer du temps à attendre que la mémoire apparaisse, et pendant ce temps, vous perdez un temps précieux.
Note importante :
Maintenant que vous avez la réponse, voici une remarque intéressante : il n'y a aucune raison inhérente pour que votre deuxième exemple soit le plus rapide. Par exemple, en Fortran, le premier exemple serait rapide et le second lent. C'est parce qu'au lieu de développer les choses en "lignes" conceptuelles comme le fait le C, Fortran développe en "colonnes", c'est-à-dire :
0,0 | 1,0 | 2,0 | 0,1 | 1,1 | 2,1 | 0,2 | 1,2 | 2,2 | 0,3 | 1,3 | 2,3
La disposition du C est appelée "row-major" et celle du Fortran "column-major". Comme vous pouvez le constater, il est très important de savoir si votre langage de programmation est de type "row-major" ou "column-major" ! Voici un lien pour plus d'informations : http://en.wikipedia.org/wiki/Row-major_order
31 votes
fr.wikipedia.org/wiki/
9 votes
Pouvez-vous ajouter des résultats de référence ?
3 votes
En rapport : stackoverflow.com/questions/9888154/
14 votes
@naught101 Les benchmarks montreront une différence de performance de 3 à 10 fois. C'est du C/C++ de base, je ne comprends pas du tout comment cela a pu obtenir autant de votes...
1 votes
Je pense que ce problème est lié au chargement de la page du tableau en mémoire par la politique de pagination du système d'exploitation.
2 votes
Pourquoi est-ce que je rate toujours les questions qui obtiennent le plus de votes ?
0 votes
Je pense que la question pourrait être non seulement pour c.
16 votes
@TC1 : Je ne pense pas que ce soit si basique ; peut-être intermédiaire. Mais il n'est pas surprenant que les questions "de base" aient tendance à être utiles à un plus grand nombre de personnes, d'où les nombreux votes positifs. De plus, c'est une question difficile à trouver sur Google, même si elle est "basique".
1 votes
J'ai ajouté ce test en tant que javascript à jsperf.com/2-boucles-réseaux-dimensionnels et vous pouvez y vérifier l'effet
0 votes
@LarsH Je n'ai jamais dit que c'était basique en général, j'ai dit que c'était basique en C/C++. Ces langages sont déjà assez avancés en eux-mêmes. Les "trucs et astuces" de ce genre viennent avec eux comme une évidence, et quiconque les utilise doit se familiariser avec eux au préalable. Il y en a quelques autres, comme l'utilisation d'incrémenteurs postfixes au lieu de préfixes sur les itérateurs STL pour éviter de faire une copie inutile, mais je suppose que vous avez compris l'essentiel. Ils vous donnent toute la corde dont vous avez besoin pour vous pendre et aucun signe que vous êtes sur le point de le faire, la plupart des autres langages (de plus haut niveau) ne vous laissent pas faire ça.
3 votes
@TC1 : le C de base est ce à quoi je faisais référence (ce problème n'est pas spécifique au C++, qui est un énorme bond en avant dans la complexité). Mais ce même problème peut s'appliquer au BASIC. "Quiconque s'en sert devrait se familiariser au préalable" - facile à dire pour une personne bien informée. Avez-vous pris connaissance de ce problème particulier avant de faire votre première boucle imbriquée avec un tableau 2D ? Je sais que je ne l'ai pas fait ! Je ne pense pas avoir utilisé des tableaux aussi grands, mais ce n'était pas parce que je savais qu'il fallait les éviter pour des raisons de performances !
0 votes
Peut être différent pour les noyaux gpu et cpu.
0 votes
Je rouvre cette question car je pense qu'il s'agit d'un meilleur "duplicata canonique" que celui qui est lié, principalement en raison de quelques réponses intéressantes postées ici.
0 votes
@naught101 Résultats de l'évaluation comparative pour N=10000 : la première méthode est 3,68 fois plus lente, 1,092s contre 0,296s. (Résultats d'une seule exécution, il y a beaucoup de variabilité entre les exécutions). La différence de vitesse dépendra probablement de la taille relative entre le cache de votre CPU et la taille du tableau choisi.
1 votes
GCC 8 et Clang 7 à venir (avec
-mllvm -enable-loopinterchange
flag) ont amélioré leurs routines d'optimisation des échanges de boucles et génèrent exactement le même assemblage dans les deux cas. gcc.godbolt.org/z/EB-PRg