Est-ce une bonne idée d'utiliser le vecteur <vecteur <double >> pour former une classe de matrice pour du code de calcul scientifique hautes performances?
37
Est-ce une bonne idée vector<vector<double>>(en utilisant std) de former une classe de matrice pour du code de calcul scientifique haute performance?
-1 Bien sûr que c'est une mauvaise idée. Vous ne pourrez pas utiliser blas, lapack ou toute autre bibliothèque de matrices existante avec un tel format de stockage. De plus, vous introduisez des inefficacités par données non locales et indirectionnelles
Thomas Klimpel
9
@Thomas Est-ce que cela justifie vraiment un vote négatif?
Akid
33
Ne pas voter. C'est une question légitime même si c'est une idée erronée.
Wolfgang Bangerth
3
std :: vector n'est pas un vecteur distribué, vous ne pourrez donc pas faire beaucoup de calculs parallèles avec ce dernier (sauf pour les machines à mémoire partagée), utilisez plutôt Petsc ou Trilinos. De plus, on utilise généralement des matrices éparses et vous stockez des matrices entièrement denses. Pour jouer avec des matrices creuses, vous pouvez utiliser un std :: vector <std :: map>, mais là encore, cela ne fonctionnerait pas très bien, voir l'article de WolfgangBangerth ci-dessous.
Gnzlbg
3
essayez d'utiliser std :: vector <std :: vector <double >> avec MPI et vous voudrez vous prendre en photo
pyCthon
Réponses:
43
C'est une mauvaise idée, car vector doit allouer autant d'objets dans l'espace qu'il y a de lignes dans votre matrice. L'allocation coûte cher, mais c'est surtout une mauvaise idée car les données de votre matrice se trouvent maintenant dans un certain nombre de tableaux dispersés dans la mémoire, plutôt que dans un seul endroit où le cache du processeur peut facilement y accéder.
C'est aussi un format de stockage inutile: std :: vector stocke deux pointeurs, l'un au début du tableau et l'autre à la fin, car la longueur du tableau est flexible. D'autre part, pour que cette matrice soit correcte, les longueurs de toutes les lignes doivent être identiques et il suffirait donc de stocker le nombre de colonnes une seule fois, plutôt que de laisser chaque ligne stocker sa longueur indépendamment.
C'est en fait pire que ce que vous dites, car std::vectorstocke en réalité trois pointeurs: le début, la fin et la fin de la région de stockage allouée (nous permettant d'appeler, par exemple .capacity()). Cette capacité peut être différente de la taille rend la situation bien pire!
user14717
18
Outre les raisons évoquées par Wolfgang, si vous utilisez a vector<vector<double> >, vous devrez le déréférencer deux fois à chaque fois que vous souhaitez récupérer un élément, ce qui est plus onéreux en calcul qu'une seule opération de déréférencement. Une approche typique consiste à allouer un seul tableau (a vector<double>ou a double *) à la place. J'ai également vu des personnes ajouter du sucre syntaxique aux classes de la matrice en encerclant ce tableau unique d'opérations d'indexation plus intuitives, afin de réduire le "surcoût mental" nécessaire pour appeler les indices appropriés.
@Wolfgang: En fonction de la taille de la matrice dense, deux pointeurs supplémentaires par ligne peuvent être négligeables. En ce qui concerne les données dispersées, on pourrait penser à utiliser un allocateur personnalisé garantissant que les vecteurs sont en mémoire contiguë. Tant que la mémoire n'est pas recyclée, même l'allocateur standard utilisera une mémoire contiguë avec un écart de taille de deux pointeurs.
@ Geoff: Si vous effectuez un accès aléatoire et utilisez un seul tableau, vous devez toujours calculer l'index. Peut-être pas plus vite.
Sur mon système, il y a maintenant un gagnant clair (Compiler gcc 4.7 avec -O3)
empreintes matricielles:
index 997:3
index 998:3
index 999:30xc7fc680xc7fc80
calc took:185.507 k=100000000
real 0m0.257s
user 0m0.244s
sys 0m0.008s
Nous voyons également que tant que l’allocateur standard ne recycle pas la mémoire libérée, les données sont contiguës. (Bien sûr, après quelques désallocations, il n'y a aucune garantie pour cela.)
temps arraymatrix imprime:
index 997:1
index 998:1
index 999:10x7ff41f208f480x7ff41f208f50
calc took:187.349 k=100000000
real 0m0.257s
user 0m0.248s
sys 0m0.004s
Vous écrivez "Sur mon système, il y a maintenant un gagnant clair" - ne voulez-vous pas dire un gagnant clair?
Akid
9
-1 Comprendre les performances du code hpc peut être non trivial. Dans votre cas, la taille de la matrice dépasse simplement la taille du cache, de sorte que vous ne faites que mesurer la bande passante mémoire de votre système. Si je change N en 200 et augmente le nombre d'itérations à 1000, j'obtiens "calcul pris: 65" par rapport à "calcul pris: 36". Si je remplace encore a = a * a par un + = a1 * a2 pour le rendre plus réaliste, je reçois "calcul pris: 176" contre "calculé: 84". Il semble donc que vous pouvez perdre un facteur deux en performances en utilisant un vecteur de vecteurs au lieu d'une matrice. La vraie vie sera plus compliquée, mais c'est toujours une mauvaise idée.
Thomas Klimpel le
oui, mais essayez d'utiliser std :: vecteur avec MPI, C gagne haut la main
pyCthon
4
Je ne le recommande pas, mais pas à cause de problèmes de performances. Ce sera un peu moins performant qu'une matrice traditionnelle, qui est généralement allouée sous la forme d'une grande quantité de données contiguës indexées à l'aide d'un seul déréférencement de pointeur et d'une arithmétique entière. La raison de la baisse des performances est principalement due aux différences de cache, mais une fois que la taille de votre matrice est suffisamment grande, cet effet est amorti. Si vous utilisez un allocateur spécial pour les vecteurs internes afin qu'ils soient alignés sur les limites du cache, cela atténue encore le problème de la mise en cache. .
Ce n'est pas en soi une raison suffisante pour ne pas le faire, à mon avis. La raison pour moi est que cela crée beaucoup de maux de tête de codage. Voici une liste des maux de tête que cela causera à long terme
Utilisation des librairies HPC
Si vous souhaitez utiliser la plupart des bibliothèques HPC, vous devrez effectuer une itération sur votre vecteur et placer toutes leurs données dans un tampon contigu, car la plupart des bibliothèques HPC attendent ce format explicite. BLAS et LAPACK me viennent à l’esprit, mais la bibliothèque MPC omniprésente MPI serait beaucoup plus difficile à utiliser.
Plus de potentiel d'erreur de codage
std::vectorne sait rien de ses entrées. Si vous remplissez std::vectorplus de std::vectors, alors votre travail consiste entièrement à vous assurer qu'ils ont tous la même taille, car rappelez-vous que nous voulons une matrice et que les matrices n'ont pas un nombre variable de lignes (ou de colonnes). Ainsi, vous devrez appeler tous les constructeurs appropriés pour chaque entrée de votre vecteur externe, et toute autre personne utilisant votre code doit résister à la tentation de l'utiliser std::vector<T>::push_back()sur l'un des vecteurs internes, ce qui provoquerait la rupture de tout le code suivant. Bien sûr, vous pouvez interdire cela si vous écrivez correctement votre classe, mais il est beaucoup plus facile de l'appliquer simplement avec une grande allocation contiguë.
Culture et attentes HPC
Les programmeurs HPC attendent simplement des données de bas niveau. Si vous leur donnez une matrice, on s’attend à ce que s’ils saisissent le pointeur sur le premier élément de la matrice et le pointeur sur le dernier élément de la matrice, tous les pointeurs entre ces deux sont valides et pointent sur des éléments de la même matrice. Ceci est similaire à mon premier point, mais différent parce que cela peut ne pas être tellement lié aux bibliothèques mais plutôt aux membres de l'équipe ou à toute personne avec laquelle vous partagez votre code.
Plus facile de raisonner sur les performances des données de niveau inférieur
Passer au niveau de représentation le plus bas de la structure de données souhaitée vous facilite la vie à long terme pour HPC. L'utilisation d'outils tels que perfet vtunevous donnera des mesures de compteur de performances de très bas niveau que vous essayerez de combiner avec les résultats de profilage traditionnels afin d'améliorer les performances de votre code. Si votre structure de données utilise beaucoup de conteneurs sophistiqués, il sera difficile de comprendre que les erreurs de cache proviennent d'un problème lié au conteneur ou d'une inefficacité de l'algorithme lui-même. Des conteneurs de code plus compliqués sont nécessaires, mais pour l'algèbre matricielle, ils ne le sont pas vraiment - vous pouvez vous en contenter 1std::vectorde stocker les données plutôt que de nstd::vectors, alors continuez.
J'écris aussi un repère. Pour une matrice de petite taille (<100 * 100), les performances sont similaires pour le vecteur <vecteur <double >> et le vecteur enveloppé 1D. Pour une matrice de grande taille (~ 1000 * 1000), le vecteur 1D enveloppé est préférable. La matrice Eigen se comporte moins bien. Je suis surpris que l'Eigen soit le pire.
Comme d'autres l'ont fait remarquer, n'essayez pas de faire des calculs avec cela ni de faire quelque chose de performant.
Cela dit, j'ai utilisé cette structure temporairement lorsque le code a besoin d'assembler un tableau à deux dimensions dont les dimensions seront déterminées lors de l'exécution et après le début du stockage des données. Par exemple, collecter des sorties de vecteurs à partir d'un processus coûteux dans lequel il n'est pas simple de calculer exactement le nombre de vecteurs à stocker au démarrage.
Vous pouvez simplement concaténer toutes vos entrées vectorielles dans un tampon au fur et à mesure de leur arrivée, mais le code sera plus durable et plus lisible si vous utilisez a vector<vector<T>>.
Réponses:
C'est une mauvaise idée, car vector doit allouer autant d'objets dans l'espace qu'il y a de lignes dans votre matrice. L'allocation coûte cher, mais c'est surtout une mauvaise idée car les données de votre matrice se trouvent maintenant dans un certain nombre de tableaux dispersés dans la mémoire, plutôt que dans un seul endroit où le cache du processeur peut facilement y accéder.
C'est aussi un format de stockage inutile: std :: vector stocke deux pointeurs, l'un au début du tableau et l'autre à la fin, car la longueur du tableau est flexible. D'autre part, pour que cette matrice soit correcte, les longueurs de toutes les lignes doivent être identiques et il suffirait donc de stocker le nombre de colonnes une seule fois, plutôt que de laisser chaque ligne stocker sa longueur indépendamment.
la source
std::vector
stocke en réalité trois pointeurs: le début, la fin et la fin de la région de stockage allouée (nous permettant d'appeler, par exemple.capacity()
). Cette capacité peut être différente de la taille rend la situation bien pire!Outre les raisons évoquées par Wolfgang, si vous utilisez a
vector<vector<double> >
, vous devrez le déréférencer deux fois à chaque fois que vous souhaitez récupérer un élément, ce qui est plus onéreux en calcul qu'une seule opération de déréférencement. Une approche typique consiste à allouer un seul tableau (avector<double>
ou adouble *
) à la place. J'ai également vu des personnes ajouter du sucre syntaxique aux classes de la matrice en encerclant ce tableau unique d'opérations d'indexation plus intuitives, afin de réduire le "surcoût mental" nécessaire pour appeler les indices appropriés.la source
Non, utilisez l’une des bibliothèques d’algèbre linéaire disponibles gratuitement. Vous trouverez une discussion sur les différentes bibliothèques ici: Recommandations pour une bibliothèque matricielle rapide et utilisable?
la source
Est-ce vraiment une si mauvaise chose?
@Wolfgang: En fonction de la taille de la matrice dense, deux pointeurs supplémentaires par ligne peuvent être négligeables. En ce qui concerne les données dispersées, on pourrait penser à utiliser un allocateur personnalisé garantissant que les vecteurs sont en mémoire contiguë. Tant que la mémoire n'est pas recyclée, même l'allocateur standard utilisera une mémoire contiguë avec un écart de taille de deux pointeurs.
@ Geoff: Si vous effectuez un accès aléatoire et utilisez un seul tableau, vous devez toujours calculer l'index. Peut-être pas plus vite.
Alors laissez-nous faire un petit test:
vectormatrix.cc:
Et maintenant, en utilisant un tableau:
arraymatrix.cc
Sur mon système, il y a maintenant un gagnant clair (Compiler gcc 4.7 avec -O3)
empreintes matricielles:
Nous voyons également que tant que l’allocateur standard ne recycle pas la mémoire libérée, les données sont contiguës. (Bien sûr, après quelques désallocations, il n'y a aucune garantie pour cela.)
temps arraymatrix imprime:
la source
Je ne le recommande pas, mais pas à cause de problèmes de performances. Ce sera un peu moins performant qu'une matrice traditionnelle, qui est généralement allouée sous la forme d'une grande quantité de données contiguës indexées à l'aide d'un seul déréférencement de pointeur et d'une arithmétique entière. La raison de la baisse des performances est principalement due aux différences de cache, mais une fois que la taille de votre matrice est suffisamment grande, cet effet est amorti. Si vous utilisez un allocateur spécial pour les vecteurs internes afin qu'ils soient alignés sur les limites du cache, cela atténue encore le problème de la mise en cache. .
Ce n'est pas en soi une raison suffisante pour ne pas le faire, à mon avis. La raison pour moi est que cela crée beaucoup de maux de tête de codage. Voici une liste des maux de tête que cela causera à long terme
Utilisation des librairies HPC
Si vous souhaitez utiliser la plupart des bibliothèques HPC, vous devrez effectuer une itération sur votre vecteur et placer toutes leurs données dans un tampon contigu, car la plupart des bibliothèques HPC attendent ce format explicite. BLAS et LAPACK me viennent à l’esprit, mais la bibliothèque MPC omniprésente MPI serait beaucoup plus difficile à utiliser.
Plus de potentiel d'erreur de codage
std::vector
ne sait rien de ses entrées. Si vous remplissezstd::vector
plus destd::vector
s, alors votre travail consiste entièrement à vous assurer qu'ils ont tous la même taille, car rappelez-vous que nous voulons une matrice et que les matrices n'ont pas un nombre variable de lignes (ou de colonnes). Ainsi, vous devrez appeler tous les constructeurs appropriés pour chaque entrée de votre vecteur externe, et toute autre personne utilisant votre code doit résister à la tentation de l'utiliserstd::vector<T>::push_back()
sur l'un des vecteurs internes, ce qui provoquerait la rupture de tout le code suivant. Bien sûr, vous pouvez interdire cela si vous écrivez correctement votre classe, mais il est beaucoup plus facile de l'appliquer simplement avec une grande allocation contiguë.Culture et attentes HPC
Les programmeurs HPC attendent simplement des données de bas niveau. Si vous leur donnez une matrice, on s’attend à ce que s’ils saisissent le pointeur sur le premier élément de la matrice et le pointeur sur le dernier élément de la matrice, tous les pointeurs entre ces deux sont valides et pointent sur des éléments de la même matrice. Ceci est similaire à mon premier point, mais différent parce que cela peut ne pas être tellement lié aux bibliothèques mais plutôt aux membres de l'équipe ou à toute personne avec laquelle vous partagez votre code.
Plus facile de raisonner sur les performances des données de niveau inférieur
Passer au niveau de représentation le plus bas de la structure de données souhaitée vous facilite la vie à long terme pour HPC. L'utilisation d'outils tels que
perf
etvtune
vous donnera des mesures de compteur de performances de très bas niveau que vous essayerez de combiner avec les résultats de profilage traditionnels afin d'améliorer les performances de votre code. Si votre structure de données utilise beaucoup de conteneurs sophistiqués, il sera difficile de comprendre que les erreurs de cache proviennent d'un problème lié au conteneur ou d'une inefficacité de l'algorithme lui-même. Des conteneurs de code plus compliqués sont nécessaires, mais pour l'algèbre matricielle, ils ne le sont pas vraiment - vous pouvez vous en contenter1
std::vector
de stocker les données plutôt que den
std::vector
s, alors continuez.la source
J'écris aussi un repère. Pour une matrice de petite taille (<100 * 100), les performances sont similaires pour le vecteur <vecteur <double >> et le vecteur enveloppé 1D. Pour une matrice de grande taille (~ 1000 * 1000), le vecteur 1D enveloppé est préférable. La matrice Eigen se comporte moins bien. Je suis surpris que l'Eigen soit le pire.
la source
Comme d'autres l'ont fait remarquer, n'essayez pas de faire des calculs avec cela ni de faire quelque chose de performant.
Cela dit, j'ai utilisé cette structure temporairement lorsque le code a besoin d'assembler un tableau à deux dimensions dont les dimensions seront déterminées lors de l'exécution et après le début du stockage des données. Par exemple, collecter des sorties de vecteurs à partir d'un processus coûteux dans lequel il n'est pas simple de calculer exactement le nombre de vecteurs à stocker au démarrage.
Vous pouvez simplement concaténer toutes vos entrées vectorielles dans un tampon au fur et à mesure de leur arrivée, mais le code sera plus durable et plus lisible si vous utilisez a
vector<vector<T>>
.la source