Ist es eine gute Idee, vector<vector<double>>
mit std eine Matrixklasse für hochperformanten wissenschaftlichen Code zu bilden?
Wenn die Antwort nein ist. Warum? Vielen Dank
Ist es eine gute Idee, vector<vector<double>>
mit std eine Matrixklasse für hochperformanten wissenschaftlichen Code zu bilden?
Wenn die Antwort nein ist. Warum? Vielen Dank
Antworten:
Dies ist eine schlechte Idee, da vector so viele Objekte im Raum zuordnen muss, wie Zeilen in Ihrer Matrix vorhanden sind. Die Zuweisung ist teuer, aber in erster Linie eine schlechte Idee, da die Daten Ihrer Matrix jetzt in einer Reihe von Arrays vorliegen, die über den Speicher verstreut sind, und nicht mehr an einem Ort, auf den der Prozessor-Cache problemlos zugreifen kann.
Es ist auch ein verschwenderisches Speicherformat: std :: vector speichert zwei Zeiger, einen zum Anfang des Arrays und einen zum Ende, da die Länge des Arrays flexibel ist. Auf der anderen Seite müssen die Längen aller Zeilen gleich sein, damit dies eine richtige Matrix ist. Es wäre also ausreichend, die Anzahl der Spalten nur einmal zu speichern, anstatt jede Zeile ihre Länge unabhängig speichern zu lassen.
std::vector
tatsächlich drei Zeiger gespeichert werden: Der Anfang, das Ende und das Ende des zugewiesenen Speicherbereichs (damit wir beispielsweise anrufen können .capacity()
). Diese Kapazität kann sich von der Größe unterscheiden, was die Situation noch viel schlimmer macht!
Zusätzlich zu den von Wolfgang genannten Gründen müssen Sie bei Verwendung von a vector<vector<double> >
die Referenz jedes Mal zweimal dereferenzieren , wenn Sie ein Element abrufen möchten. Dies ist rechenintensiver als eine einzelne Dereferenzierungsoperation. Ein typischer Ansatz besteht darin, stattdessen ein einzelnes Array (a vector<double>
oder a double *
) zuzuweisen . Ich habe auch Leute gesehen, die syntaktischen Zucker zu Matrixklassen hinzugefügt haben, indem sie einige intuitivere Indizierungsoperationen um dieses einzelne Array gewickelt haben, um den "mentalen Overhead" zu reduzieren, der zum Aufrufen der richtigen Indizes erforderlich ist.
Nein, verwenden Sie eine der frei verfügbaren Bibliotheken für lineare Algebra. Eine Diskussion über verschiedene Bibliotheken finden Sie hier: Empfehlungen für eine brauchbare, schnelle C ++ - Matrixbibliothek?
Ist es wirklich so schlimm?
@Wolfgang: Abhängig von der Größe der dichten Matrix können zwei zusätzliche Zeiger pro Zeile vernachlässigbar sein. Bei verstreuten Daten könnte man sich einen benutzerdefinierten Allokator vorstellen, der sicherstellt, dass sich die Vektoren im zusammenhängenden Speicher befinden. Solange der Speicher nicht recycelt wird, verwendet selbst der Standard-Allokator zusammenhängenden Speicher mit einer Lücke von zwei Zeigern.
@Geoff: Wenn Sie einen Direktzugriff durchführen und nur ein Array verwenden, müssen Sie den Index noch berechnen. Könnte nicht schneller sein.
Also lasst uns einen kleinen Test machen:
vectormatrix.cc:
#include<vector>
#include<iostream>
#include<random>
#include <functional>
#include <sys/time.h>
int main()
{
int N=1000;
struct timeval start, end;
std::cout<< "Checking differenz between last entry of previous row and first entry of this row"<<std::endl;
std::vector<std::vector<double> > matrix(N, std::vector<double>(N, 0.0));
for(std::size_t i=1; i<N;i++)
std::cout<< "index "<<i<<": "<<&(matrix[i][0])-&(matrix[i-1][N-1])<<std::endl;
std::cout<<&(matrix[0][N-1])<<" "<<&(matrix[1][0])<<std::endl;
gettimeofday(&start, NULL);
int k=0;
for(int j=0; j<100; j++)
for(std::size_t i=0; i<N;i++)
for(std::size_t j=0; j<N;j++, k++)
matrix[i][j]=matrix[i][j]*matrix[i][j];
gettimeofday(&end, NULL);
double seconds = end.tv_sec - start.tv_sec;
double useconds = end.tv_usec - start.tv_usec;
double mtime = ((seconds) * 1000 + useconds/1000.0) + 0.5;
std::cout<<"calc took: "<<mtime<<" k="<<k<<std::endl;
std::normal_distribution<double> normal_dist(0, 100);
std::mt19937 engine; // Mersenne twister MT19937
auto generator = std::bind(normal_dist, engine);
for(std::size_t i=1; i<N;i++)
for(std::size_t j=1; j<N;j++)
matrix[i][j]=generator();
}
Und jetzt mit einem Array:
arraymatrix.cc
#include<vector>
#include<iostream>
#include<random>
#include <functional>
#include <sys/time.h>
int main()
{
int N=1000;
struct timeval start, end;
std::cout<< "Checking difference between last entry of previous row and first entry of this row"<<std::endl;
double* matrix=new double[N*N];
for(std::size_t i=1; i<N;i++)
std::cout<< "index "<<i<<": "<<(matrix+(i*N))-(matrix+(i*N-1))<<std::endl;
std::cout<<(matrix+N-1)<<" "<<(matrix+N)<<std::endl;
int NN=N*N;
int k=0;
gettimeofday(&start, NULL);
for(int j=0; j<100; j++)
for(double* entry =matrix, *endEntry=entry+NN;
entry!=endEntry;++entry, k++)
*entry=(*entry)*(*entry);
gettimeofday(&end, NULL);
double seconds = end.tv_sec - start.tv_sec;
double useconds = end.tv_usec - start.tv_usec;
double mtime = ((seconds) * 1000 + useconds/1000.0) + 0.5;
std::cout<<"calc took: "<<mtime<<" k="<<k<<std::endl;
std::normal_distribution<double> normal_dist(0, 100);
std::mt19937 engine; // Mersenne twister MT19937
auto generator = std::bind(normal_dist, engine);
for(std::size_t i=1; i<N*N;i++)
matrix[i]=generator();
}
Auf meinem System gibt es jetzt klare Gewinner (Compiler gcc 4.7 mit -O3)
Zeitvektormatrix druckt:
index 997: 3
index 998: 3
index 999: 3
0xc7fc68 0xc7fc80
calc took: 185.507 k=100000000
real 0m0.257s
user 0m0.244s
sys 0m0.008s
Wir sehen auch, dass die Daten zusammenhängend sind, solange der Standard-Allokator den freigegebenen Speicher nicht wiederverwendet. (Natürlich gibt es nach einigen Aufhebungen keine Garantie dafür.)
Zeitarraymatrix druckt:
index 997: 1
index 998: 1
index 999: 1
0x7ff41f208f48 0x7ff41f208f50
calc took: 187.349 k=100000000
real 0m0.257s
user 0m0.248s
sys 0m0.004s
Ich empfehle es nicht, aber nicht wegen Leistungsproblemen. Es ist etwas weniger performant als eine herkömmliche Matrix, die normalerweise als großer Teil zusammenhängender Daten zugeordnet wird, die mit einer einzigen Zeiger-Dereferenzierung und einer Ganzzahl-Arithmetik indiziert werden. Der Grund für die Leistungsbeeinträchtigung sind hauptsächlich Unterschiede im Caching. Wenn Ihre Matrixgröße jedoch groß genug ist, wird dieser Effekt amortisiert. Wenn Sie einen speziellen Allokator für die inneren Vektoren verwenden, damit diese an den Cache-Grenzen ausgerichtet sind, wird das Caching-Problem weiter verringert .
Das allein ist meiner Meinung nach kein Grund genug, es nicht zu tun. Der Grund für mich ist, dass es viele Probleme mit der Programmierung verursacht. Hier ist eine Liste der Kopfschmerzen, die dies langfristig verursachen wird
Wenn Sie die meisten HPC-Bibliotheken verwenden möchten, müssen Sie über Ihren Vektor iterieren und alle ihre Daten in einem zusammenhängenden Puffer ablegen, da die meisten HPC-Bibliotheken dieses explizite Format erwarten. BLAS und LAPACK kommen in den Sinn, aber auch die allgegenwärtige HPC-Bibliothek MPI wäre viel schwieriger zu verwenden.
std::vector
weiß nichts über seine Einträge. Wenn Sie ein std::vector
mit mehr std::vector
s füllen, ist es Ihre Aufgabe, sicherzustellen, dass alle die gleiche Größe haben, denn denken Sie daran, dass wir eine Matrix und Matrizen ohne variable Anzahl von Zeilen (oder Spalten) wollen. Daher müssen Sie für jeden Eintrag Ihres äußeren Vektors alle korrekten Konstruktoren aufrufen, und jeder andere, der Ihren Code verwendet, muss sich der Versuchung widersetzen, std::vector<T>::push_back()
einen der inneren Vektoren zu verwenden, was dazu führen würde, dass der gesamte folgende Code beschädigt wird. Natürlich können Sie dies ablehnen, wenn Sie Ihre Klasse korrekt schreiben, aber es ist viel einfacher, dies einfach mit einer großen zusammenhängenden Zuordnung zu erzwingen.
HPC-Programmierer erwarten einfach Daten auf niedriger Ebene. Wenn Sie ihnen eine Matrix geben, besteht die Erwartung, dass, wenn sie den Zeiger auf das erste Element der Matrix und einen Zeiger auf das letzte Element der Matrix aufgenommen haben, alle Zeiger zwischen diesen beiden gültig sind und auf Elemente desselben verweisen Matrix. Dies ist ähnlich zu meinem ersten Punkt, aber anders, weil es nicht so sehr mit Bibliotheken zu tun hat, sondern eher mit Teammitgliedern oder jemandem, mit dem Sie Ihren Code teilen.
Wenn Sie sich auf die unterste Ebene der gewünschten Datenstruktur begeben, wird HPC auf lange Sicht das Leben erleichtern. Wenn Sie Tools wie perf
und verwenden vtune
, erhalten Sie Leistungsindikatormessungen auf sehr niedrigem Niveau, die Sie mit herkömmlichen Profilerstellungsergebnissen kombinieren, um die Leistung Ihres Codes zu verbessern. Wenn Ihre Datenstruktur viele ausgefallene Container verwendet, ist es schwer zu verstehen, dass Cache-Fehler auf ein Problem mit dem Container oder auf eine Ineffizienz des Algorithmus selbst zurückzuführen sind. Für kompliziertere Codecontainer sind sie notwendig, für die Matrixalgebra jedoch nicht - Sie können damit auskommen, nur 1
std::vector
die Daten anstatt der n
std::vector
s zu speichern .
Ich schreibe auch einen Benchmark. Für eine Matrix kleiner Größe (<100 * 100) ist die Leistung für einen Vektor <Vektor <doppelt >> und einen umwickelten 1D-Vektor ähnlich. Für eine Matrix mit großer Größe (~ 1000 * 1000) ist der umhüllte 1D-Vektor besser. Die Eigenmatrix verhält sich schlechter. Es überrascht mich, dass das Eigen das Schlimmste ist.
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <map>
#include <vector>
#include <string>
#include <cmath>
#include <numeric>
#include "time.h"
#include <chrono>
#include <cstdlib>
#include <Eigen/Dense>
using namespace std;
using namespace std::chrono; // namespace for recording running time
using namespace Eigen;
int main()
{
const int row = 1000;
const int col = row;
const int N = 1e8;
// 2D vector
auto start = high_resolution_clock::now();
vector<vector<double>> vec_2D(row,vector<double>(col,0.));
for (int i = 0; i < N; i++)
{
for (int i=0; i<row; i++)
{
for (int j=0; j<col; j++)
{
vec_2D[i][j] *= vec_2D[i][j];
}
}
}
auto stop = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(stop - start);
cout << "2D vector: " << duration.count()/1e6 << " s" << endl;
// 2D array
start = high_resolution_clock::now();
double array_2D[row][col];
for (int i = 0; i < N; i++)
{
for (int i=0; i<row; i++)
{
for (int j=0; j<col; j++)
{
array_2D[i][j] *= array_2D[i][j];
}
}
}
stop = high_resolution_clock::now();
duration = duration_cast<microseconds>(stop - start);
cout << "2D array: " << duration.count() / 1e6 << " s" << endl;
// wrapped 1D vector
start = high_resolution_clock::now();
vector<double> vec_1D(row*col, 0.);
for (int i = 0; i < N; i++)
{
for (int i=0; i<row; i++)
{
for (int j=0; j<col; j++)
{
vec_1D[i*col+j] *= vec_1D[i*col+j];
}
}
}
stop = high_resolution_clock::now();
duration = duration_cast<microseconds>(stop - start);
cout << "1D vector: " << duration.count() / 1e6 << " s" << endl;
// eigen 2D matrix
start = high_resolution_clock::now();
MatrixXd mat(row, col);
for (int i = 0; i < N; i++)
{
for (int j=0; j<col; j++)
{
for (int i=0; i<row; i++)
{
mat(i,j) *= mat(i,j);
}
}
}
stop = high_resolution_clock::now();
duration = duration_cast<microseconds>(stop - start);
cout << "2D eigen matrix: " << duration.count() / 1e6 << " s" << endl;
}
Wie andere betont haben, versuchen Sie nicht, damit zu rechnen oder etwas Performantes zu tun.
Trotzdem habe ich diese Struktur als temporäres Element verwendet, wenn der Code ein 2D-Array zusammenstellen muss, dessen Abmessungen zur Laufzeit und nach dem Beginn der Datenspeicherung ermittelt werden. Beispielsweise können Sie Vektorausgaben aus einem teuren Prozess erfassen, bei dem es nicht einfach ist, genau zu berechnen, wie viele Vektoren Sie beim Start speichern müssen.
Sie könnten einfach alle Ihre Vektoreingaben in einem Puffer zusammenfassen, wenn sie eingehen, aber der Code ist haltbarer und lesbarer, wenn Sie a verwenden vector<vector<T>>
.