用于在c / c ++

时间:2017-05-17 16:20:12

标签: c++ arrays matrix cache-locality

很久以前,受C"中的"数字配方的启发,我开始使用以下构造来存储矩阵(2D阵列)。

double **allocate_matrix(int NumRows, int NumCol)
{
  double **x;
  int i;

  x = (double **)malloc(NumRows * sizeof(double *));
  for (i = 0; i < NumRows; ++i) x[i] = (double *)calloc(NumCol, sizeof(double));
  return x;
}

double **x = allocate_matrix(1000,2000);
x[m][n] = ...;

但最近注意到许多人实现了如下矩阵

double *x = (double *)malloc(NumRows * NumCols * sizeof(double));
x[NumCol * m + n] = ...;

从地方的角度来看,第二种方法看起来很完美,但可读性很差......所以我开始怀疑,这是我的第一个存储辅助数组或**double指针的方法真的很糟糕,或者编译器会优化最终它会在性能上与第二种方法大致相同吗?我很怀疑,因为我认为在第一种方法中,在访问值x[m]然后x[m][n]时会进行两次跳转,并且每次CPU首先加载{{1}时都有可能}数组然后x数组。

P.S。不要担心存储x[m]的额外内存,对于大型矩阵,这只是一小部分。

P.P.S。因为很多人都不太了解我的问题,所以我会尝试重新塑造它:我是否理解第一种方法是地方性 - 地狱,每当**double首次访​​问x[m][n]时将数组加载到CPU缓存中,然后加载x数组,从而使每次访问都以与RAM通信的速度进行。或者我错了,第一种方法也可以从数据位置的角度来看?

5 个答案:

答案 0 :(得分:3)

对于C风格的分配,您实际上可以充分利用这两个方面:

double **allocate_matrix(int NumRows, int NumCol)
{
  double **x;
  int i;

  x = (double **)malloc(NumRows * sizeof(double *));
  x[0] = (double *)calloc(NumRows * NumCol, sizeof(double)); // <<< single contiguous memory allocation for entire array
  for (i = 1; i < NumRows; ++i) x[i] = x[i - 1] + NumCols;
  return x;
}

通过这种方式,您可以获得数据位置及其相关的缓存/内存访问优势,并且可以互换地将数组视为double **或扁平2D数组(array[i * NumCols + j])。您的calloc / free来电也较少(2NumRows + 1)。

答案 1 :(得分:1)

无需猜测编译器是否会优化第一种方法。只需使用您知道的第二种方法,并使用实现例如这些方法的包装类:

double& operator(int x, int y);
double const& operator(int x, int y) const;

...并按如下方式访问您的对象:

arr(2, 3) = 5;

或者,如果您可以在包装器类中承受更多代码复杂性,则可以实现可以使用更传统的arr[2][3] = 5;语法访问的类。这是在Boost.MultiArray库中以维度无关的方式实现的,但您也可以使用代理类来执行自己的简单实现。

注意:考虑您对C样式的使用(硬编码非泛型&#34; double&#34;类型,普通指针,函数开始变量声明和malloc) ,在实现我提到的任何一个选项之前,你可能需要更多地学习C ++构造。

答案 2 :(得分:1)

这两种方法完全不同。

  • 虽然第一种方法允许通过添加另一个间接(double**数组)来更容易地直接访问值,因此您需要1 + N个mallocs,...
  • 第二种方法保证所有值都是连续存储的,只需要一个malloc。

我认为第二种方法总是优越的。 Malloc是一项昂贵的操作,连续的内存是一个巨大的优势,具体取决于应用程序。

在C ++中,您只需按照以下方式实现它:

std::vector<double> matrix(NumRows * NumCols);
matrix[y * numCols + x] = value;  // Access

如果您担心自己必须计算索引的不便,请添加一个实现operator(int x, int y)的包装器。

在访问值时,第一种方法更昂贵,这也是对的。因为您需要两个内存查找,如您所述x[m]然后x[m][n]。编译器无法对此进行优化&#34;。第一个数组将根据其大小进行缓存,性能可能不会那么差。在第二种情况下,您需要额外的乘法才能直接访问。

答案 3 :(得分:0)

在您使用的第一个方法中,主数组中的double*指向逻辑列(大小为NumCol的数组)。

所以,如果你写下面的内容,你会从某种意义上获得数据局部性的好处(伪代码):

foreach(row in rows):
    foreach(elem in row):
        //Do something

如果您使用第二种方法尝试了同样的事情,并且如果元素访问按照您指定的方式完成(即x[NumCol*m + n]),您仍然可以获得相同的好处。这是因为您将数组视为行主要顺序。如果你在按主列顺序访问元素的同时尝试了相同的伪代码,我认为如果数组大小足够大,你会得到缓存未命中。

除此之外,第二种方法具有额外的理想属性,即成为单个连续的内存块,即使循环遍历多行,也可以进一步提高性能(与第一种方法不同)。

因此,总之,第二种方法在性能方面应该要好得多。

答案 4 :(得分:0)

如果NumCol是编译时常量,或者如果您正在使用启用了语言扩展的GCC,那么您可以这样做:

double (*x)[NumCol] = (double (*)[NumCol]) malloc(NumRows * sizeof (double[NumCol]));

然后使用x作为2D数组,编译器将为您执行索引算法。需要注意的是,除非NumCol是编译时常量,否则ISO C ++不会允许你这样做,如果你使用GCC语言扩展,你将无法将代码移植到另一个编译器。