使用vector <vector <double >>形成高性能科学计算代码的矩阵类是一个好主意吗?


36

使用vector<vector<double>>(使用std)形成高性能科学计算代码的矩阵类是一个好主意吗?

如果答案是否定的。为什么?谢谢


2
-1当然是个坏主意。您将无法使用带有这种存储格式的blas,lapack或任何其他现有的矩阵库。此外,还通过引入数据非localty和间接效率低下
托马斯Klimpel

9
@Thomas真的需要投票吗?
2012年

33
不要投票。即使是一个错误的想法,这也是一个合理的问题。
Wolfgang Bangerth,2012年

3
std :: vector不是分布式矢量,因此您将无法使用它进行大量并行计算(共享存储计算机除外),请改用Petsc或Trilinos。此外,通常会处理稀疏矩阵,您将要存储完整的密集矩阵。要使用稀疏矩阵,可以使用std :: vector <std :: map>,但这又不能很好地执行,请参见下面的@WolfgangBangerth帖子。
gnzlbg 2012年

3
尝试使用带有MPI的std :: vector <std :: vector <double >>,您将希望拍摄自己的照片
pyCthon 2012年

Answers:


42

这是一个坏主意,因为向量需要在空间中分配与矩阵中的行一样多的对象。分配是昂贵的,但是首先这是一个坏主意,因为矩阵的数据现在存在于分散在内存中的多个数组中,而不是全部存在于处理器缓存可以轻松访问的位置。

这也是一种浪费的存储格式:std :: vector存储两个指针,一个指向数组的开头,另一个指向结尾,因为数组的长度是灵活的。另一方面,要使其成为适当的矩阵,所有行的长度必须相同,因此仅存储一次列数就足够了,而不是让每一行独立存储其长度就足够了。


实际上,这比您说的还要糟糕,因为std::vector实际上存储了三个指针:分配的存储区域的开始,结尾和结尾(例如,允许我们调用.capacity())。容量可能与大小不同,这会使情况变得更加糟糕!
user14717

18

除了Wolfgang提到的原因之外,如果使用a vector<vector<double> >,则每次要检索元素时都必须对其进行两次取消引用,这比单次取消引用操作在计算上更为昂贵。一种典型的方法是分配单个数组(a vector<double>或a double *)。我还看到人们通过将一些更直观的索引操作包裹在单个数组周围,从而在矩阵类中添加了语法糖,以减少调用适当索引所需的“精神开销”。



4

真的是一件坏事吗?

@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

您写下“在我的系统上现在有明显的赢家”-您是说没有明显的赢家?
AKID

9
-1了解hpc代码的性能可能并不容易。在您的情况下,矩阵的大小仅超过高速缓存的大小,因此您只是在测量系统的内存带宽。如果我将N更改为200,并将迭代次数增加到1000,则得到“计算得出:65”与“计算得出:36”。如果我进一步将a = a * a替换为a + = a1 * a2以使其更逼真,则会得到“计算所得:176”与“计算所得:84”。因此,看起来您可以通过使用向量向量而不是矩阵来使性能损失两倍。现实生活会更加复杂,但这仍然不是一个好主意。
Thomas Klimpel 2012年

是的,但是尝试将std :: vectors与MPI结合使用,C胜出了
pyCthon 2012年

3

我不建议这样做,但不是因为性能问题。它的性能将比传统矩阵差一些,传统矩阵通常被分配为一大块连续数据,这些数据使用单个指针取消引用和整数算术进行索引。性能下降的原因主要是缓存差异,但是一旦矩阵大小足够大,此效果就会被摊销,如果您对内部向量使用特殊的分配器以使它们与缓存边界对齐,则可以进一步缓解缓存问题。

我认为,仅凭这一点本身还不足以做到这一点。对我而言,原因是它造成了很多编码难题。这是长期头痛的清单

使用HPC库

如果要使用大多数HPC库,则需要遍历向量并将其所有数据放置在连续的缓冲区中,因为大多数HPC库都需要这种显式格式。想到了BLAS和LAPACK,但是无处不在的HPC库MPI会更难使用。

潜在的编码错误

std::vector对它的条目一无所知。如果您std::vector用更多的std::vectors 填充,那么确保它们都具有相同的大小完全是您的工作,因为请记住,我们想要一个矩阵,而矩阵没有可变数量的行(或列)。因此,您必须为外部向量的每个条目调用所有正确的构造函数,并且其他使用您的代码的人都必须抵制std::vector<T>::push_back()在任何内部向量上使用的诱惑,这将导致随后的所有代码中断。当然,如果您正确地编写类,则可以禁止这样做,但是仅通过大量连续分配来强制实施就容易得多。

HPC文化和期望

HPC程序员只是希望获得底层数据。如果给他们一个矩阵,则期望他们抓住了指向矩阵第一个元素的指针和指向矩阵最后一个元素的指针,那么这两个指针之间的所有指针都是有效的,并且指向相同的元素矩阵。这与我的第一点相似,但有所不同,因为它可能与库没有太大关系,而是与团队成员或与您共享代码的任何人有很大关系。

更容易推断较低级别数据的性能

从长远来看,将所需数据结构的表示降到最低级别可以使您的生活更轻松。使用perf和这样的工具vtune会给您带来非常低级的性能计数器测量结果,您将尝试将它们与传统的性能分析结果结合起来以提高代码的性能。如果您的数据结构使用大量精美的容器,将很难理解缓存丢失是由于容器问题或算法本身效率低下造成的。对于更复杂的代码容器来说是必需的,但是对于矩阵代数而言,它们实际上不是必需的-您可以仅1 std::vector存储数据而不是n std::vectors来解决这个问题。


0

我还写了一个基准。对于小尺寸(<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;
}

0

正如其他人指出的那样,请勿尝试使用它进行数学运算或执行任何高性能的运算。

就是说,当代码需要组装一个2D数组时,我将这种结构用作临时结构,其尺寸将在运行时以及开始存储数据之后确定。例如,从某个昂贵的过程中收集向量输出,而要精确地计算启动时需要存储多少个向量并不容易。

您可以将所有向量输入在输入时串联到一个缓冲区中,但是如果使用,代码将更持久且可读性更好vector<vector<T>>

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.