使用vector<vector<double>>
(使用std)形成高性能科学计算代码的矩阵类是一个好主意吗?
如果答案是否定的。为什么?谢谢
使用vector<vector<double>>
(使用std)形成高性能科学计算代码的矩阵类是一个好主意吗?
如果答案是否定的。为什么?谢谢
Answers:
这是一个坏主意,因为向量需要在空间中分配与矩阵中的行一样多的对象。分配是昂贵的,但是首先这是一个坏主意,因为矩阵的数据现在存在于分散在内存中的多个数组中,而不是全部存在于处理器缓存可以轻松访问的位置。
这也是一种浪费的存储格式:std :: vector存储两个指针,一个指向数组的开头,另一个指向结尾,因为数组的长度是灵活的。另一方面,要使其成为适当的矩阵,所有行的长度必须相同,因此仅存储一次列数就足够了,而不是让每一行独立存储其长度就足够了。
std::vector
实际上存储了三个指针:分配的存储区域的开始,结尾和结尾(例如,允许我们调用.capacity()
)。容量可能与大小不同,这会使情况变得更加糟糕!
不,请使用免费的线性代数库之一。有关不同库的讨论可以在这里找到:对可用的快速C ++矩阵库的建议?
真的是一件坏事吗?
@Wolfgang:根据密集矩阵的大小,每行两个额外的指针可以忽略不计。关于分散的数据,可以考虑使用一种自定义分配器,以确保向量在连续内存中。只要不回收内存,即使标准分配器也将使用两个指针大小的间隙作为连续内存。
@Geoff:如果您要进行随机访问,并且仅使用一个数组,则仍然需要计算索引。可能不会更快。
因此,让我们做一个小测试:
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();
}
现在使用一个数组:
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程序员只是希望获得底层数据。如果给他们一个矩阵,则期望他们抓住了指向矩阵第一个元素的指针和指向矩阵最后一个元素的指针,那么这两个指针之间的所有指针都是有效的,并且指向相同的元素矩阵。这与我的第一点相似,但有所不同,因为它可能与库没有太大关系,而是与团队成员或与您共享代码的任何人有很大关系。
从长远来看,将所需数据结构的表示降到最低级别可以使您的生活更轻松。使用perf
和这样的工具vtune
会给您带来非常低级的性能计数器测量结果,您将尝试将它们与传统的性能分析结果结合起来以提高代码的性能。如果您的数据结构使用大量精美的容器,将很难理解缓存丢失是由于容器问题或算法本身效率低下造成的。对于更复杂的代码容器来说是必需的,但是对于矩阵代数而言,它们实际上不是必需的-您可以仅1
std::vector
存储数据而不是n
std::vector
s来解决这个问题。
我还写了一个基准。对于小尺寸(<100 * 100)的矩阵,对于vector <vector <double >>和包装的1D vector,性能相似。对于大尺寸矩阵(〜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;
}