vector<vector<double>>
(std を使用して)高性能の科学計算コードのマトリックスクラスを形成するのは良い考えですか?
答えがノーの場合。どうして?ありがとう
vector<vector<double>>
(std を使用して)高性能の科学計算コードのマトリックスクラスを形成するのは良い考えですか?
答えがノーの場合。どうして?ありがとう
回答:
行列内の行と同じ数のオブジェクトを空間に割り当てる必要があるため、これは悪い考えです。割り当てはコストがかかりますが、プロセッサキャッシュが簡単にアクセスできる1つの場所ではなく、メモリの周りに散らばった多数の配列にマトリックスのデータが存在するため、主に悪い考えです。
無駄なストレージ形式でもあります。std:: vectorは、配列の長さが柔軟であるため、配列の先頭と末尾に2つのポインターを格納します。一方、これが適切な行列であるためには、すべての行の長さが同じでなければならないため、各行にその長さを個別に格納させるのではなく、列の数を一度だけ格納すれば十分です。
std::vector
は、割り当てられたストレージ領域の開始点、終了点、終了点の3つのポインターを格納するため、あなたが言うよりも悪いです(たとえば、を呼び出すことができます.capacity()
)。その容量はサイズとは異なる場合があるため、状況はさらに悪化します!
Wolfgangが言及した理由に加えて、aを使用する場合vector<vector<double> >
、要素を取得するたびに2回参照解除する必要があり、単一の逆参照操作よりも計算コストがかかります。典型的なアプローチの1つはvector<double>
、double *
代わりに単一の配列(a またはa )を割り当てることです。適切なインデックスを呼び出すために必要な「メンタルオーバーヘッド」の量を減らすために、この単一の配列をより直感的なインデックス付け操作でラップすることで、マトリックスクラスに構文糖を追加する人もいます。
いいえ、無料の線形代数ライブラリのいずれかを使用します。さまざまなライブラリについての議論はここで見つけることができます:使用可能な高速C ++マトリックスライブラリの推奨事項
それは本当に悪いことですか?
@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
推奨しませんが、パフォーマンスの問題のためではありません。通常、単一のポインター逆参照と整数演算を使用してインデックス付けされる連続したデータの大きなチャンクとして割り当てられる従来のマトリックスよりもパフォーマンスが少し低下します。パフォーマンスヒットの理由は主にキャッシュの違いですが、マトリックスサイズが十分に大きくなると、この効果は償却され、内部ベクトルに特別なアロケーターを使用して、キャッシュの境界に合わせて配置すると、キャッシュの問題がさらに軽減されます。
私の意見では、それだけではそれをしない理由にはなりません。私の理由は、多くのコーディングの頭痛の種を作成することです。これが長期的な原因となる頭痛のリストです
ほとんどのHPCライブラリを使用する場合、ほとんどのHPCライブラリはこの明示的な形式を想定しているため、ベクトルを反復処理し、すべてのデータを連続したバッファに配置する必要があります。BLASとLAPACKが思い浮かびますが、ユビキタスHPCライブラリMPIの使用ははるかに困難です。
std::vector
そのエントリについて何も知りません。std::vector
をさらにstd::vector
sで埋める場合、行列と行列には可変数の行(または列)がないことを思い出してくださいので、すべてが同じサイズであることを確認するのは完全にあなたの仕事です。したがって、外部ベクトルのすべてのエントリに対してすべての正しいコンストラクターを呼び出す必要があります。コードを使用する他のユーザーstd::vector<T>::push_back()
は、内部ベクトルのいずれかを使用する誘惑に抵抗しなければなりません。もちろん、クラスを正しく記述すればこれを禁止できますが、単純に大きな連続した割り当てでこれを強制する方がはるかに簡単です。
HPCプログラマーは単に低レベルのデータを期待しています。それらにマトリックスを与えると、マトリックスの最初の要素へのポインターとマトリックスの最後の要素へのポインターをつかんだ場合、これら2つの間のすべてのポインターが有効で、同じ要素を指すことが期待されますマトリックス。これは私の最初のポイントと似ていますが、ライブラリとはあまり関係がなく、チームメンバーやコードを共有する人とは関係がないため、異なります。
目的のデータ構造の最下位レベルの表現にドロップすると、HPCの長期的な生活が楽になります。perf
やなどのツールを使用vtune
すると、非常に低レベルのパフォーマンスカウンター測定値が得られます。これを従来のプロファイリング結果と組み合わせて、コードのパフォーマンスを向上させます。データ構造が多くの派手なコンテナを使用している場合、コンテナの問題やアルゴリズム自体の非効率性が原因でキャッシュミスが発生していることを理解するのは困難です。より複雑なコードコンテナには必要ですが、行列代数には実際には必要ありません。s 1
std::vector
ではなくデータを保存するだけで十分ですn
std::vector
。
ベンチマークも書いています。サイズが小さい(<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;
}