Ist es eine gute Idee, vector <vector <double >> zu verwenden, um eine Matrixklasse für Hochleistungs-Code für das wissenschaftliche Rechnen zu bilden?


37

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


2
-1 Natürlich ist es eine schlechte Idee. Sie können weder Blas noch Lapack oder eine andere vorhandene Matrixbibliothek mit einem solchen Speicherformat verwenden. Darüber hinaus führen Sie Ineffizienzen durch Nicht-Lokalisierung und Indirektion von Daten ein
Thomas Klimpel,

9
@Thomas Ist das wirklich eine Ablehnung wert?
akid

33
Nicht abstimmen. Es ist eine berechtigte Frage, auch wenn es sich um eine falsche Idee handelt.
Wolfgang Bangerth

3
std :: vector ist kein verteilter Vektor, daher können Sie nicht viel paralleles Rechnen damit ausführen (mit Ausnahme von gemeinsam genutzten Speichermaschinen). Verwenden Sie stattdessen Petsc oder Trilinos. Außerdem befasst man sich normalerweise mit spärlichen Matrizen und man würde vollständig dichte Matrizen speichern. Zum Spielen mit spärlichen Matrizen könnten Sie eine std :: vector <std :: map> verwenden, aber auch dies würde keine gute Leistung bringen, siehe @WolfgangBangerth Post unten.
gnzlbg

3
versuchen Sie es mit std :: vector <std :: vector <double >> mit MPI und Sie wollen , um sich selbst zu schießen
pyCthon

Antworten:


43

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.


Es ist tatsächlich schlimmer als Sie sagen, weil std::vectortatsä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!
user14717

18

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.



5

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

Sie schreiben "Auf meinem System gibt es jetzt eindeutige Gewinner" - meinten Sie keinen eindeutigen Gewinner?
akid

9
-1 Das Verständnis der Leistung von HPC-Code kann nicht trivial sein. In Ihrem Fall überschreitet die Größe der Matrix einfach die Cache-Größe, sodass Sie nur die Speicherbandbreite Ihres Systems messen. Wenn ich N in 200 ändere und die Anzahl der Iterationen auf 1000 erhöhe, erhalte ich "calc took: 65" vs "calc took: 36". Wenn ich weiter a = a * a durch a + = a1 * a2 ersetze, um es realistischer zu machen, erhalte ich "calc took: 176" vs "calc took: 84". Es sieht also so aus, als ob Sie einen Faktor zwei an Leistung verlieren können, wenn Sie einen Vektor von Vektoren anstelle einer Matrix verwenden. Das wirkliche Leben wird komplizierter, aber es ist immer noch eine schlechte Idee.
Thomas Klimpel

ja, aber versuchen Sie es mit std :: vectors mit MPI, C gewinnt
zweifellos

4

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

Verwendung von HPC-Bibliotheken

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.

Mehr Potenzial für Codierungsfehler

std::vectorweiß nichts über seine Einträge. Wenn Sie ein std::vectormit mehr std::vectors 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-Kultur und Erwartungen

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.

Einfachere Begründung für die Leistung von Daten auf niedrigerer Ebene

Wenn Sie sich auf die unterste Ebene der gewünschten Datenstruktur begeben, wird HPC auf lange Sicht das Leben erleichtern. Wenn Sie Tools wie perfund 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::vectordie Daten anstatt der n std::vectors zu speichern .


1

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;
}

0

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>>.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.