4 votes

Pourquoi ces vitesses de calcul sont-elles différentes pour les tableaux multidimensionnels en C++ ?

Il s'agit peut-être d'une question répétitive. N'hésitez donc pas à la signaler si vous le souhaitez. En C++, j'ai appris que les dimensions des tableaux sont stockées consécutivement dans la mémoire. Comment les tableaux 3D sont-ils stockés en C ? J'ai donc fait une petite expérience pour assigner des nombres naturels à une matrice de taille 1600000000x1 et 1x1600000000 (veuillez changer le nom de la matrice). matsize dans le code à une valeur plus petite en fonction de votre mémoire). Le code ci-dessous attribue un nombre naturel de 1 à 1600000000 à la matrice a (dont les dimensions sont 1x1600000000) et calcule la somme des cubes de tous les éléments. Le cas contraire consiste simplement à inverser les dimensions de la matrice, ce que je fais en modifiant xdim a matsize y ydim à 1, et recompiler le code et l'exécuter à nouveau. La matrice est [xdim][ydim]

#include <iostream>
#include <time.h>
using namespace std;
int main()
{
long int matsize, i, j, xdim, ydim;
long double ss;
double** a;
double time1, time2, time3;
clock_t starttime = clock();    
matsize=1600000000;
xdim=1;
ydim=matsize;   
ss=0.0;    
a= new double *[xdim];
for(i=0;i<xdim;i++)
{
    a[i]= new double[ydim];
}

time1= (double)( clock() - starttime ) / (double)CLOCKS_PER_SEC;    
cout << "allocated. time taken for allocation was " << time1 <<" seconds. computation started" << endl;    
for(i=0;i<xdim;i++)
{
    for(j=0;j<ydim;j++)
    {
        a[i][j]=(i+1)*(j+1);
        ss=ss+a[i][j]*a[i][j]*a[i][j];
    }
}
cout << "last number is " << a[xdim-1][ydim-1] << " . sum is " << ss << endl;
time2= ((double)( clock() - starttime ) / (double)CLOCKS_PER_SEC) - time1;
cout << "computation done. time taken for computation was " << time2 << " seconds" << endl;    
for(i=0;i<xdim;i++)
{
    delete [] a[i];
}
delete [] a;   
time3= ((double)( clock() - starttime ) / (double)CLOCKS_PER_SEC) - time2;
cout << "deallocated. time taken for deallocation was " << time3 << " seconds" << endl;    
cout << "the total time taken is " << (double)( clock() - starttime ) / (double)CLOCKS_PER_SEC << endl;
cout << "or " << time1+time2+time3 << " seconds" << endl; 
return 0;
}

Mes résultats pour les deux cas sont -

Cas 1 : xdim=1 et ydim=1600000000

alloué. le temps pris pour l'allocation était de 4.5e-05 secondes. le calcul a commencé le dernier nombre est 1.6e+09 . la somme est 1.6384e+36 Le calcul est terminé. Le temps de calcul a duré 14,7475 secondes. deallocated. le temps pris pour la deallocation était de 0.875754 secondes le temps total est de 15.6233 ou 15.6233 secondes

Cas 2 : xdim=1600000000 et ydim=1

alloué. le temps pris pour l'allocation était de 56.1583 secondes. le calcul a commencé le dernier chiffre est 1.6e+09 . la somme est 1.6384e+36 Le calcul est terminé. Le temps de calcul a été de 50,7347 secondes. deallocated. le temps de deallocation est de 270.038 secondes. le temps total est de 320.773 ou 376.931 secondes

La somme de sortie est la même dans les deux cas. Je peux comprendre que le temps pris pour l'allocation et la désallocation de la mémoire est différent dans les deux cas, mais pourquoi le temps de calcul est-il aussi différent si l'allocation de la mémoire est continue ? Quel est le problème dans ce code ?

Si cela a de l'importance, j'utilise g++ sur Mountain Lion et je compile en utilisant g++ -std=c++11, processeur i7 quad core, 16 Go de RAM.

5voto

Tony D Points 43962

Chaque vecteur individuel stocke le contenu de manière contiguë, y compris le vecteur des pointeurs vers les vecteurs, mais les adresses des appels successifs que vous faites à new ne sont pas contiguës, ni les appels que le vecteur fait en interne pour créer son tampon. Ainsi, si vous avez un énorme vecteur de pointeurs vers de minuscules vecteurs, votre mémoire est effectivement non contiguë et vous n'obtiendrez pas de bons résultats dans le cache. Si vous avez un vecteur à un seul élément vers un énorme vecteur, alors la mémoire est contiguë et le cache fonctionnera bien.

Visuellement, la disposition rapide/contiguë est :

*a--[0]
.    |
.   [0][1][2][3][4][5][...]

Votre alternative lente est :

.        [0]     [0]
.         \       /
*a--[0][1][2][3][4][5][...]
.    |   \    |     \
.   [0]   \[0][0]   [0]

Les tableaux multidimensionnels peuvent être créés sur la pile en utilisant, par exemple, les fonctions suivantes

int x[10][20];

Dans ce cas, la mémoire sera contiguë, avec la mémoire dans chacun des x[0], x[1] etc. contigus. (Ainsi, x[0][0] est avant x[0][1], et non immédiatement avant x[1][0]).

Pour disposer efficacement d'un tableau multidimensionnel contigu sur le tas, vous devez créer un vecteur avec le produit des dimensions souhaitées, puis écrire une classe enveloppe qui multiplie les dimensions pour trouver un élément particulier.

3voto

dasblinkenlight Points 264350

Le temps de calcul est différent en raison de la mise en cache des données. Connaître le localité de référence Lorsque vous lisez un emplacement en mémoire, le CPU charge des données provenant d'adresses adjacentes. Il prédit que la prochaine lecture se fera à partir de l'emplacement situé quelques octets plus loin que l'adresse que vous venez de lire.

Lorsque le tableau est alloué en tant que [1][N] les éléments sont effectivement stockés consécutivement, de sorte que les prédictions du CPU se réalisent presque toujours. Les données dont vous avez besoin sont presque toujours disponibles dans le cache du CPU, qui est plusieurs fois plus rapide que la mémoire principale. Le CPU continue à charger les emplacements situés en avant de ceux que vous venez de lire pendant qu'il effectue le calcul, de sorte que le chargement des nouvelles données et l'addition des nombres se poursuivent en parallèle.

Lorsque vous intervertissez les dimensions, les nombres que vous additionnez ne sont plus à des endroits consécutifs. Cela est dû au fait que les appels consécutifs à new n'alloue pas de données dans des régions de mémoire consécutives : les bibliothèques de gestion de la mémoire ajoutent plusieurs octets à des fins de "comptabilité", et allouent toujours des morceaux de mémoire d'une taille minimale, souvent supérieure à double . Lorsque vous demandez un morceau plus petit que le minimum, la région allouée est remplie. En conséquence, votre double peuvent finir par être séparés de 20 octets dans le meilleur des cas. * - suffisante pour annuler les effets de la lecture anticipée d'emplacements mémoire adjacents. C'est pourquoi l'unité centrale est obligée d'attendre pendant qu'elle charge les données à partir d'un autre emplacement. Cela ralentit plusieurs fois le calcul.


* Dans le pire des cas, les valeurs pourraient être placées arbitrairement loin, en fonction des allocations et des désallocations effectuées avant l'exécution de votre code.

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