很久以前,受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通信的速度进行。或者我错了,第一种方法也可以从数据位置的角度来看?
答案 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
来电也较少(2
与NumRows + 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是一项昂贵的操作,连续的内存是一个巨大的优势,具体取决于应用程序。
在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语言扩展,你将无法将代码移植到另一个编译器。