this主题的作者声称访问从具有固定长度的2D数组转换的1D数组比访问原始2D数组要快得多,至少在C#中是这样。我想知道这是否也适用于C / C ++。
使用3D数组时,取消引用指向数组的指针三次取得(x,y,z)的值:
int val = arr[x][y][z];
但是你可以将数组转换为1D数组并计算每个坐标的索引, 所以代码变成了:
int val = arr[SIZE_X * SIZE_Y * z + SIZE_X * y + x];
这将通过一次解除引用和3次乘法以及2次加法来取代三种解除引用操作。
问题是:解除引用比计算坐标索引慢三倍或更快?
基准测试输出:
3 dimensions: 5s
1 dimension: 14s
1 dimension fast: 4s
代码:
#include <iostream>
#include <time.h>
int main(int argc, char** argv)
{
const int SIZE_X = 750, SIZE_Y = SIZE_X, SIZE_Z = SIZE_X;
const int SIZE_XY = SIZE_X * SIZE_Y;
time_t startTime;
// 3 dimensions
time(&startTime);
int ***array3d = new int **[SIZE_X];
for (int x = 0; x < SIZE_X; ++x)
{
array3d[x] = new int *[SIZE_Y];
for (int y = 0; y < SIZE_Y; ++y)
array3d[x][y] = new int[SIZE_Z];
}
for (int x = 0; x < SIZE_X; ++x)
for (int y = 0; y < SIZE_Y; ++y)
for (int z = 0; z < SIZE_Z; ++z)
array3d[x][y][z] = 0;
for (int x = 0; x < SIZE_X; ++x)
{
for (int y = 0; y < SIZE_Y; ++y)
delete[] array3d[x][y];
delete[] array3d[x];
}
std::cout << "3 dimensions: " << time(0) - startTime << "s\n";
time(&startTime);
int *array1d = new int[SIZE_X * SIZE_Y * SIZE_Z];
for (int x = 0; x < SIZE_X; ++x)
for (int y = 0; y < SIZE_Y; ++y)
for (int z = 0; z < SIZE_Z; ++z)
array1d[x + SIZE_X * y + SIZE_XY * z] = 0;
delete[] array1d;
std::cout << "1 dimension: " << time(0) - startTime << "s\n";
time(&startTime);
array1d = new int[SIZE_X * SIZE_Y * SIZE_Z];
int i = 0;
for (int x = 0; x < SIZE_X; ++x)
for (int y = 0; y < SIZE_Y; ++y)
for (int z = 0; z < SIZE_Z; ++z)
array1d[++i] = 0;
delete[] array1d;
std::cout << "1 dimension fast: " << time(0) - startTime << "s\n";
return 0;
}
结果:3d比1维数组的快速版本更快,速度稍慢。
编辑: 我将1维数组循环更改为:
for (int z = 0; z < SIZE_Z; ++z)
for (int y = 0; y < SIZE_Y; ++y)
for (int x = 0; x < SIZE_X; ++x)
array1d[x + SIZE_X * y + SIZE_XY * z] = 0;
它只用了5秒钟,和3d变体一样快。
因此,访问顺序很重要,而不是维度。我想。
答案 0 :(得分:3)
很抱歉答案很长。
更多关于内存访问模式。但首先,关于基准测试:
new
和delete
,它们应该在外面。现在回到数组。首先,在给定的示例中,应该使用memset
,而不是重新发明轮子。我知道它是出于测试目的,但在这种情况下,最好使用例如rand()
(尽管值应该降低,因为rand比= 0慢得多,测试时间太长)。但无论如何,这就是:
在三维版本中,您的最内层循环访问线性数组。这是非常缓存友好和快速的方式。每次循环迭代都不会执行解除引用,因为编译器会发现它无法更改。因此,最常用的代码行 - 最内层循环 - 访问线性存储器阵列。
'快速'版本的1d阵列做同样的事情。也好。 memset
仍然更好,但是: - )。
但是当涉及'慢'1d版本时,事情就搞砸了。查看您的索引行:array1d[x + SIZE_X * y + SIZE_XY * z] = 0;
。最内层循环迭代z
,因此在每次迭代时都要设置veeeeery far int。这种访问模式只会使数据缓存无效,并且大多数时候程序只是等待数据写入内存。但是,如果将其更改为array1d[SIZE_XY * x + SIZE_X * y + z] = 0;
,它将再次成为线性阵列访问,因此变得非常快。另外,如果你愿意,可以在外循环中计算加法的左侧部分,可能会使其更快一些。
但是1d数组的真正优点在于它可以从头到尾线性访问。如果使用它的算法可以重新排列,以这种方式遍历数组 - 这是双赢的场景。
如果您想对其进行测试,只需将您的3D版本中的[x][y][z]
订单更改为[z][y][x]
,即可显着降低性能。
所以,关于最初的问题 - 答案是'它取决于'。最重要的是它取决于数据访问模式,还取决于许多其他因素,例如数组维度的实际深度,每个维度的大小,支持效果的频率,如new / delete等等。但是,如果你可以线性化数据访问 - 它会很快,但在这种情况下你不需要3D,对吗?
(是的,我显然支持手动计算索引的一维数组,所以算我有偏见。抱歉)。
答案 1 :(得分:1)
为什么不直接查看每个选项的反汇编并查找?
当然,反汇编取决于使用的编译器,而编译器又取决于CPU架构及其支持的操作。
这实际上是这里最重要的声明,因为每个选项可能有其优点和缺点,具体取决于您的平台(编译器,链接器,处理器)。
因此,如果没有指定底层平台,可能就手头的一般问题没有决定性的答案。
以下答案分为两种情况。
在每种情况下,它都会检查两个选项(一维数组和三维数组),使用Microsoft Visual C ++ 2010为Pentium E5200编译的每个选项的反汇编作为示例。
#define X 10
#define Y 10
#define Z 10
int val = array3d[x][y][z];
mov eax,dword ptr [x]
imul eax,eax,190h
add eax,dword ptr [array3d]
mov ecx,dword ptr [y]
imul ecx,ecx,28h
add eax,ecx
mov edx,dword ptr [z]
mov eax,dword ptr [eax+edx*4]
mov dword ptr [val],eax
int val = array1d[x+X*y+X*Y*z];
mov eax,dword ptr [y]
imul eax,eax,0Ah
add eax,dword ptr [x]
mov ecx,dword ptr [z]
imul ecx,ecx,64h
add eax,ecx
mov edx,dword ptr [array1d]
mov eax,dword ptr [edx+eax*4]
mov dword ptr [val],eax
正如您所看到的,“数学”略有不同,但除此之外,这两个选项实际上是相同的。因此,唯一可能影响性能的是运行时缓存,但我不知道这两个选项中的任何一个在这方面如何明显优于其他选项。
#define X 10
#define Y 10
#define Z 10
int val = array3d[x][y][z];
mov eax,dword ptr [x]
mov ecx,dword ptr [array3d]
mov edx,dword ptr [ecx+eax*4]
mov eax,dword ptr [y]
mov ecx,dword ptr [edx+eax*4]
mov edx,dword ptr [z]
mov eax,dword ptr [ecx+edx*4]
mov dword ptr [val],eax
int val = array1d[x+X*y+X*Y*z];
mov eax,dword ptr [y]
imul eax,eax,0Ah
add eax,dword ptr [x]
mov ecx,dword ptr [z]
imul ecx,ecx,64h
add eax,ecx
mov edx,dword ptr [array1d]
mov eax,dword ptr [edx+eax*4]
mov dword ptr [val],eax
这一次,结果明显不同,但很难确定哪一个(如果有的话)始终优于另一个。使用3D阵列时,与使用1D阵列相比,似乎有更多 Load (mov
)操作。因此,此处的运行时性能高度依赖于每个阵列在内存中的位置(RAM,L2缓存等)。