È una buona idea usare vector <vector <double>> per formare una classe di matrici per codice di calcolo scientifico ad alte prestazioni?


37

È una buona idea usare vector<vector<double>>(usando std) per formare una classe di matrici per codice scientifico scientifico ad alte prestazioni?

Se la risposta è no. Perché? Grazie


2
-1 Naturalmente è una cattiva idea. Non sarai in grado di usare blas, lapack o qualsiasi altra libreria di matrici esistente con un tale formato di archiviazione. Inoltre, si introducono inefficienze in base alla non localizzazione e all'indirizzamento dei dati
Thomas Klimpel,

9
@Thomas Questo merita davvero un downvote?
Akid

33
Non sottovalutare. È una domanda legittima anche se è un'idea sbagliata.
Wolfgang Bangerth,

3
std :: vector non è un vettore distribuito, quindi non sarai in grado di fare molto calcolo parallelo con esso (tranne che per le macchine a memoria condivisa), usa invece Petsc o Trilinos. Inoltre, di solito si tratta di matrici sparse e si memorizzeranno matrici piene. Per giocare con matrici sparse potresti usare uno std :: vector <std :: map> ma, di nuovo, questo non funzionerebbe molto bene, vedi il post di @WolfgangBangerth di seguito.
gnzlbg,

3
prova a usare std :: vector <std :: vector <double>> con MPI e vorrai sparare da solo
pyCthon

Risposte:


43

È una cattiva idea perché il vettore deve allocare tanti oggetti nello spazio quante sono le righe nella tua matrice. L'allocazione è costosa, ma soprattutto è una cattiva idea perché i dati della tua matrice ora esistono in una serie di array sparsi nella memoria, piuttosto che in tutti in un posto in cui la cache del processore può facilmente accedervi.

È anche un formato di archiviazione dispendioso: std :: vector memorizza due puntatori, uno all'inizio dell'array e uno alla fine perché la lunghezza dell'array è flessibile. D'altra parte, affinché questa sia una matrice corretta, le lunghezze di tutte le righe devono essere le stesse e quindi sarebbe sufficiente memorizzare il numero di colonne una sola volta, piuttosto che lasciare che ciascuna riga memorizzi la sua lunghezza in modo indipendente.


In realtà è peggio di quello che dici, perché in std::vectorrealtà memorizza tre puntatori: l'inizio, la fine e la fine della regione di archiviazione allocata (che ci consente di chiamare, ad esempio, .capacity()). Quella capacità può essere diversa dalle dimensioni rende la situazione molto peggiore!
user14717

18

Oltre ai motivi menzionati da Wolfgang, se usi a vector<vector<double> >, dovrai dereferenziarlo due volte ogni volta che desideri recuperare un elemento, che è più costoso dal punto di vista computazionale di una singola operazione di dereferenziamento. Un approccio tipico è invece quello di allocare un singolo array (a vector<double>o a double *). Ho anche visto persone aggiungere zucchero sintattico alle classi di matrici avvolgendo attorno a questo singolo array alcune operazioni di indicizzazione più intuitive, per ridurre la quantità di "sovraccarico mentale" necessaria per invocare gli indici corretti.



5

È davvero una brutta cosa?

@Wolfgang: a seconda della dimensione della matrice densa, due puntatori aggiuntivi per riga potrebbero essere trascurabili. Per quanto riguarda i dati sparsi, si potrebbe pensare di utilizzare un allocatore personalizzato che si assicuri che i vettori siano nella memoria contigua. Finché la memoria non viene riciclata, anche l'allocatore standard ci farà memoria contigua con un gap di due puntatori.

@Geoff: se si sta effettuando un accesso casuale e si utilizza solo un array, è comunque necessario calcolare l'indice. Potrebbe non essere più veloce.

Quindi facciamo un piccolo test:

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

E ora usando un 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();
}

Sul mio sistema ora c'è un chiaro vincitore (compilatore gcc 4.7 con -O3)

stampe tempo vectormatrix:

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

Vediamo anche che finché l'allocatore standard non ricicla la memoria libera, i dati sono contigui. (Naturalmente dopo alcune deallocazioni non c'è garanzia per questo.)

stampe time arraymatrix:

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

Scrivi "Sul mio sistema ora c'è un chiaro vincitore" - non intendevi un chiaro vincitore?
Akid

9
-1 La comprensione delle prestazioni del codice HPC può essere non banale. Nel tuo caso, la dimensione della matrice supera semplicemente la dimensione della cache, quindi stai solo misurando la larghezza di banda di memoria del tuo sistema. Se cambio N a 200 e aumento il numero di iterazioni a 1000, ottengo "calc take: 65" vs "calc take: 36". Se sostituisco ulteriormente a = a * a con a = = a1 * a2 per renderlo più realistico, ottengo "calc take: 176" vs "calc take: 84". Quindi sembra che tu possa perdere un fattore due nelle prestazioni usando un vettore di vettori invece di una matrice. La vita reale sarà più complicata, ma è comunque una cattiva idea.
Thomas Klimpel,

sì, ma prova a usare std :: vectors con MPI, C vince a mani
basse

4

Non lo consiglio, ma non a causa di problemi di prestazioni. Sarà un po 'meno performante di una matrice tradizionale, che di solito viene allocata come una grande porzione di dati contigui che viene indicizzata usando una singola dereferenza del puntatore e l'aritmetica dei numeri interi. Il motivo del riscontro delle prestazioni è principalmente la differenza nella memorizzazione nella cache, ma una volta che la dimensione della matrice diventa abbastanza grande, questo effetto verrà ammortizzato e se si utilizza un allocatore speciale per i vettori interni in modo che siano allineati ai limiti della cache, questo mitiga ulteriormente il problema della memorizzazione nella cache .

Questo da solo non è una ragione sufficiente per non farlo, secondo me. La ragione per me è che crea molti mal di testa da codice. Ecco un elenco di mal di testa che questo causerà a lungo termine

Uso delle librerie HPC

Se si desidera utilizzare la maggior parte delle librerie HPC, è necessario scorrere il proprio vettore e posizionare tutti i relativi dati in un buffer contiguo, poiché la maggior parte delle librerie HPC prevede questo formato esplicito. BLAS e LAPACK vengono in mente, ma anche l'onnipresente libreria MPI HPC sarebbe molto più difficile da usare.

Più potenziale per errori di codifica

std::vectornon sa nulla delle sue voci. Se si riempie un carattere std::vectorcon più std::vectors, allora è interamente compito tuo assicurarsi che abbiano tutti le stesse dimensioni, perché ricorda che vogliamo una matrice e che le matrici non hanno un numero variabile di righe (o colonne). Quindi dovrai chiamare tutti i costruttori corretti per ogni voce del tuo vettore esterno e chiunque altro usi il tuo codice deve resistere alla tentazione di usare std::vector<T>::push_back()su uno qualsiasi dei vettori interni, il che provocherebbe la rottura di tutto il codice seguente. Ovviamente puoi impedirlo se scrivi correttamente la tua lezione, ma è molto più semplice imporlo semplicemente con una grande allocazione contigua.

Cultura e aspettative HPC

I programmatori HPC si aspettano semplicemente dati di basso livello. Se dai loro una matrice, ci si aspetta che se prendessero il puntatore al primo elemento della matrice e un puntatore all'ultimo elemento della matrice, allora tutti i puntatori tra questi due sono validi e puntano a elementi dello stesso matrice. Questo è simile al mio primo punto, ma diverso perché potrebbe non essere così tanto correlato alle biblioteche ma piuttosto ai membri del team o a chiunque condivida il codice.

Più facile ragionare sulle prestazioni dei dati di livello inferiore

Il passaggio al livello più basso della struttura dei dati desiderata semplifica la vita a lungo termine per HPC. L'uso di strumenti come perfe vtuneti fornirà misurazioni del contatore delle prestazioni di livello molto basso che proverai a combinare con i risultati di profilazione tradizionali al fine di migliorare le prestazioni del tuo codice. Se la tua struttura di dati utilizza molti contenitori fantasiosi, sarà difficile capire che i problemi di cache provengono da un problema con il contenitore o da un'inefficienza dell'algoritmo stesso. Per i contenitori di codice più complicati sono necessari, ma per l'algebra matriciale in realtà non lo sono - puoi cavartela solo 1 std::vectorper memorizzare i dati piuttosto che n std::vectors, quindi procedi con quello.


1

Scrivo anche un benchmark. Per matrici di piccole dimensioni (<100 * 100), le prestazioni sono simili per il vettore <vettore <doppio >> e il vettore 1D avvolto. Per matrici di grandi dimensioni (~ 1000 * 1000), il vettore 1D avvolto è migliore. La matrice di Eigen si comporta peggio. Mi sorprende che l'Eigen sia il peggiore.

#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

Come altri hanno sottolineato, non provare a fare matematica con esso o fare qualcosa di performante.

Detto questo, ho usato questa struttura come temporanea quando il codice ha bisogno di assemblare un array 2-D le cui dimensioni saranno determinate in fase di esecuzione e dopo aver iniziato a memorizzare i dati. Ad esempio, raccogliere output vettoriali da un processo costoso in cui non è semplice calcolare esattamente quanti vettori è necessario memorizzare all'avvio.

Potresti semplicemente concatenare tutti i tuoi input vettoriali in un buffer appena entrano, ma il codice sarà più durevole e più leggibile se usi a vector<vector<T>>.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.