一维或三维数组
1 or 3 dimensional array?
这个主题的作者声称访问从固定长度的2D数组转换成的1D数组比访问原始2D数组要快得多,至少在c#中是这样。我想知道这是否也适用于C/c++。
当使用3D数组时,在(x, y, z)处的值是通过对指向该数组的指针解引用三次来获取的:
int val = arr[x][y][z];
但是你可以将数组转换为一维数组并计算每个坐标的索引,因此代码变成:
int val = arr[SIZE_X * SIZE_Y * z + SIZE_X * y + x];
这将用1个解引用操作、3个乘法操作和2个加法操作代替3个解引用操作。
问题是:解引用是比计算坐标索引慢三倍还是快三倍?
基准测试输出: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 << "sn";
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 << "sn";
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 << "sn";
return 0;
}
结果:3d比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版本一样快。
所以访问的顺序很重要,而不是维数。我认为。
抱歉,我的回答太长了。
更多的是关于内存访问模式。但首先,关于基准测试:
- 在基准测试时,不要计算秒,因为秒太长了。至少使用毫秒。
- 不包括你不想测试的部分到基准部分-在给定的例子中,它是
new
和delete
,他们应该在外面。 - 改变基准测试的顺序可能会产生不同的结果,因为缓存利用率
- 确保所有基准测试版本遵循相同的算法(如果您测试的是实现,而不是算法本身)。在给定的例子中这部分是不正确的,我稍后会解释。
现在回到数组。首先,在给定的例子中,应该使用memset
,而不是重新发明轮子。我知道这是为了测试目的,但在这种情况下,最好使用例如rand()
(虽然值应该降低,因为rand比=0慢得多,测试需要很长时间)。但没关系,它是这样的:
在三维版本中,最内层循环访问线性数组。这是非常缓存友好和快速的方式。解引用不是在每次循环迭代中执行的,因为编译器看到它不能改变。因此,最常用的代码行——最内层循环——访问线性内存数组。
'fast'版本的1d数组做同样的事情。也很好。memset
仍然更好:-)。
但是当涉及到"慢"的1d版本时,事情就混乱了。看看索引行:array1d[x + SIZE_X * y + SIZE_XY * z] = 0;
。最内层循环迭代z
,因此在每次迭代中设置veeeerery int。这种访问模式使数据缓存毫无用处,大多数时候程序只是等待数据被写入内存。但是,如果将其更改为array1d[SIZE_XY * x + SIZE_X * y + z] = 0;
,则再次变为线性数组访问,因此变得非常快。另外,如果你愿意,可以在外循环中计算加法的左部分,这可能会使它更快一些。
但1d数组的真正伟大之处在于它可以从头到尾线性访问。如果使用的算法可能会以这种方式重新排列以遍历数组-这是双赢的情况。
如果你想测试它,只需将3d版本中的[x][y][z]
顺序更改为[z][y][x]
,即可看到性能显着降低。
那么,关于第一个问题,答案是"看情况"。最重要的是,它取决于数据访问模式,但也取决于许多其他因素,如数组维度的实际深度、每个维度的大小、支持效果(如new/delete)的频率,等等。但如果你能线性化数据访问——它已经很快了,但在这种情况下,你不需要3D,对吧?
(是的,我显然赞成手动计算索引的1D数组,所以我有偏见。对不起)。
您为什么不直接查看每个选项的反汇编并找出答案呢?
当然,反汇编取决于所使用的编译器,而编译器又取决于CPU体系结构及其支持的操作。
这实际上是这里最重要的语句,因为每个选项可能比其他选项有自己的优点和缺点,这取决于您的平台(编译器,链接器,处理器)。
因此,如果不指定底层平台,可能无法对手头的一般问题给出决定性的答案。
下面的答案分为两种情况。
在每种情况下,它检查两个选项(1D-array和3D-array),使用Microsoft Visual c++ 2010为Pentium E5200编译的每个选项的反汇编作为示例。
Case #1 -静态分配数组
#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
正如您所看到的,"数学"略有不同,但除此之外,这两个选项实际上是相同的。因此,这里唯一可能影响性能的是运行时缓存,尽管我不知道这两个选项中哪一个在这方面有明显的优势。
Case #2 -动态分配数组
#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缓存等)。
- 多维数组存储三种不同的数据类型?
- 为什么我需要三个嵌套的大括号来调用赋值运算符,将const引用到二维数组
- 三维数组中的C/C++DWORD到BYTE和BYTE到DWORD的转换
- std::将三维数组复制到三维向量中
- 显示结构的三维数组
- 如何将X 2维数组连接到一个三维阵列中
- 使用unique_ptr来管理三维数组
- 无法访问三维数组中的特定位置
- 内存相关崩溃:Cocos2d游戏中的三维数组
- 如何在三维数组中使用std::map来存储/检索具有特定值的键
- 使用参数在C++中实例化三维数组
- 三维数组作为数组的向量
- 用C++将三维数组转换为二维数组
- C/C++ 三维数组如何在内存中存储,以及遍历它的最快方法是什么
- 三维数组c++中的访问冲突(使用malloc)
- 二维数组:二维数组的子平方的元素之和
- 用变量作为C++中二维数组的维数的问题
- 读取数组2维的另一种方式c++
- 有四个括号的数组的维数是多少?
- 在c++中实现多维数组奇维的最佳方法