vector <vector <double >>を使用して、高性能の科学計算コードのマトリックスクラスを形成するのは良い考えですか?


37

vector<vector<double>>(std を使用して)高性能の科学計算コードのマトリックスクラスを形成するのは良い考えですか?

答えがノーの場合。どうして?ありがとう


2
-1もちろん、それは悪い考えです。このようなストレージ形式では、blas、lapack、またはその他の既存のマトリックスライブラリを使用できません。さらに、データの非局所性と間接性による非効率性を導入します
Thomas Klimpel

9
@Thomasそれは本当に下票を正当化しますか?
-akid

33
降格しないでください。見当違いのアイデアであっても、それは正当な質問です。
ウルフギャングバンガース

3
std :: vectorは分散ベクトルではないため、(共有メモリマシンを除く)並列処理をあまり行えません。代わりにPetscまたはTrilinosを使用してください。さらに、通常はスパース行列を扱い、完全に密な行列を保存します。スパース行列で遊ぶ場合は、std :: vector <std :: map>を使用できますが、これでもパフォーマンスはあまり良くありません。以下の@WolfgangBangerthの投稿を参照してください。
gnzlbg

3
MPIでstd :: vector <std :: vector <double >>を使用してみて、自分を撮影したいと思うでしょう
-pyCthon

回答:


43

行列内の行と同じ数のオブジェクトを空間に割り当てる必要があるため、これは悪い考えです。割り当てはコストがかかりますが、プロセッサキャッシュが簡単にアクセスできる1つの場所ではなく、メモリの周りに散らばった多数の配列にマトリックスのデータが存在するため、主に悪い考えです。

無駄なストレージ形式でもあります。std:: vectorは、配列の長さが柔軟であるため、配列の先頭と末尾に2つのポインターを格納します。一方、これが適切な行列であるためには、すべての行の長さが同じでなければならないため、各行にその長さを個別に格納させるのではなく、列の数を一度だけ格納すれば十分です。


実際にstd::vectorは、割り当てられたストレージ領域の開始点、終了点、終了点の3つのポインターを格納するため、あなたが言うよりも悪いです(たとえば、を呼び出すことができます.capacity())。その容量はサイズとは異なる場合があるため、状況はさらに悪化します!
user14717

18

Wolfgangが言及した理由に加えて、aを使用する場合vector<vector<double> >、要素を取得するたびに2回参照解除する必要があり、単一の逆参照操作よりも計算コストがかかります。典型的なアプローチの1つはvector<double>double *代わりに単一の配列(a またはa )を割り当てることです。適切なインデックスを呼び出すために必要な「メンタルオーバーヘッド」の量を減らすために、この単一の配列をより直感的なインデックス付け操作でラップすることで、マトリックスクラスに構文糖を追加する人もいます。



5

それは本当に悪いことですか?

@Wolfgang:密行列のサイズによっては、行ごとに2つの追加ポインターが無視できる場合があります。分散データに関しては、ベクトルが連続したメモリにあることを確認するカスタムアロケーターの使用を考えることができます。メモリがリサイクルされない限り、標準のアロケータでさえ、2つのポインタサイズのギャップを持つ連続したメモリを使用します。

@Geoff:ランダムアクセスを行っており、配列を1つだけ使用する場合は、インデックスを計算する必要があります。速くないかもしれません。

それでは、小さなテストをしましょう。

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

そして今、1つの配列を使用しています:

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

私のシステムには明確な勝者がいます(-O3を使用したコンパイラgcc 4.7)

時間ベクトル行列印刷:

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

また、標準のアロケータが解放されたメモリをリサイクルしない限り、データは連続していることがわかります。(もちろん、いくつかの割り当て解除の後、これに対する保証はありません。)

時間配列行列印刷:

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

「私のシステムには明確な勝者がいる」と書いていますが明確な勝者はいないということですか
akid

9
-1 hpcコードのパフォーマンスを理解することは簡単ではありません。あなたの場合、マトリックスのサイズは単にキャッシュサイズを超えているため、システムのメモリ帯域幅を測定しているだけです。Nを200に変更し、反復回数を1000に増やすと、「calc takes:65」vs「calc takes:36」になります。さらにa = a * aをa + = a1 * a2に置き換えてより現実的にすると、「calc takes:176」vs「calc takes:84」になります。そのため、行列の代わりにベクトルのベクトルを使用することで、パフォーマンスの2倍を失うことができるようです。実生活はもっと複雑になりますが、それでも悪い考えです。
トーマスクリンペル

ええ、std :: vectorsをMPIで使用してみてください。Cが勝ちます
pyCthon

4

推奨しませんが、パフォーマンスの問題のためではありません。通常、単一のポインター逆参照と整数演算を使用してインデックス付けされる連続したデータの大きなチャンクとして割り当てられる従来のマトリックスよりもパフォーマンスが少し低下します。パフォーマンスヒットの理由は主にキャッシュの違いですが、マトリックスサイズが十分に大きくなると、この効果は償却され、内部ベクトルに特別なアロケーターを使用して、キャッシュの境界に合わせて配置すると、キャッシュの問題がさらに軽減されます。

私の意見では、それだけではそれをしない理由にはなりません。私の理由は、多くのコーディングの頭痛の種を作成することです。これが長期的な原因となる頭痛のリストです

HPCライブラリの使用

ほとんどのHPCライブラリを使用する場合、ほとんどのHPCライブラリはこの明示的な形式を想定しているため、ベクトルを反復処理し、すべてのデータを連続したバッファに配置する必要があります。BLASとLAPACKが思い浮かびますが、ユビキタスHPCライブラリMPIの使用ははるかに困難です。

コーディングエラーの可能性が高い

std::vectorそのエントリについて何も知りません。std::vectorをさらにstd::vectorsで埋める場合、行列と行列には可変数の行(または列)がないことを思い出してくださいので、すべてが同じサイズであることを確認するのは完全にあなたの仕事です。したがって、外部ベクトルのすべてのエントリに対してすべての正しいコンストラクターを呼び出す必要があります。コードを使用する他のユーザーstd::vector<T>::push_back()は、内部ベクトルのいずれかを使用する誘惑に抵抗しなければなりません。もちろん、クラスを正しく記述すればこれを禁止できますが、単純に大きな連続した割り当てでこれを強制する方がはるかに簡単です。

HPCの文化と期待

HPCプログラマーは単に低レベルのデータを期待しています。それらにマトリックスを与えると、マトリックスの最初の要素へのポインターとマトリックスの最後の要素へのポインターをつかんだ場合、これら2つの間のすべてのポインターが有効で、同じ要素を指すことが期待されますマトリックス。これは私の最初のポイントと似ていますが、ライブラリとはあまり関係がなく、チームメンバーやコードを共有する人とは関係がないため、異なります。

低レベルのデータのパフォーマンスについて推論しやすい

目的のデータ構造の最下位レベルの表現にドロップすると、HPCの長期的な生活が楽になります。perfやなどのツールを使用vtuneすると、非常に低レベルのパフォーマンスカウンター測定値が得られます。これを従来のプロファイリング結果と組み合わせて、コードのパフォーマンスを向上させます。データ構造が多くの派手なコンテナを使用している場合、コンテナの問題やアルゴリズム自体の非効率性が原因でキャッシュミスが発生していることを理解するのは困難です。より複雑なコードコンテナには必要ですが、行列代数には実際には必要ありません。s 1 std::vectorではなくデータを保存するだけで十分ですn std::vector


1

ベンチマークも書いています。サイズが小さい(<100 * 100)行列の場合、パフォーマンスはvector <vector <double >>およびラップされた1Dベクトルの場合と同様です。大きなサイズ(〜1000 * 1000)のマトリックスの場合、ラップされた1Dベクトルの方が適しています。固有行列の動作は悪くなります。固有値が最悪であることは驚きです。

#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

他の人が指摘しているように、それを使って数学をやったり、何かパフォーマンスをしようとしないでください。

とはいえ、実行時およびデータの格納を開始した後に次元が決定される2次元配列をコードでアセンブルする必要がある場合、この構造を一時的なものとして使用しました。たとえば、起動時に保存する必要のあるベクトルの数を正確に計算するのは簡単ではない、高価なプロセスからベクトル出力を収集します。

すべてのベクトル入力を入力時に1つのバッファーに連結することもできますが、を使用するとコードの耐久性と可読性が向上しますvector<vector<T>>

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.