一维或三维数组

1 or 3 dimensional array?

本文关键字:数组 三维 一维      更新时间:2023-10-16

这个主题的作者声称访问从固定长度的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版本一样快。

所以访问的顺序很重要,而不是维数。我认为。

抱歉,我的回答太长了。

更多的是关于内存访问模式。但首先,关于基准测试:

  • 在基准测试时,不要计算秒,因为秒太长了。至少使用毫秒。
  • 不包括你不想测试的部分到基准部分-在给定的例子中,它是newdelete,他们应该在外面。
  • 改变基准测试的顺序可能会产生不同的结果,因为缓存利用率
  • 确保所有基准测试版本遵循相同的算法(如果您测试的是实现,而不是算法本身)。在给定的例子中这部分是不正确的,我稍后会解释。

现在回到数组。首先,在给定的例子中,应该使用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缓存等)。