【问题标题】:1 or 3 dimensional array?1维还是3维数组?
【发布时间】:2014-06-20 19:55:59
【问题描述】:

this 主题的作者声称,访问从具有固定长度的二维数组转换而来的一维数组比访问原始二维数组要快得多,至少在 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];

这会将三个取消引用操作替换为一次取消引用和 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 维数组的快速版本更快,只是稍微慢了一点。

编辑: 我将一维数组循环更改为:

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 版本一样快。

所以访问顺序很重要,而不是维度。我想。

【问题讨论】:

  • 有意义的事情并不总是正确的。此类声明需要仔细审查。
  • 这就是我问你的原因。
  • 我不确定为什么这被标记为不清楚 - 他在问是否在他的第一个代码块中访问arr 的给定元素更快,或者在他的第二个代码块。似乎是一个有效的问题,尽管他可以自己调查。
  • 我平衡了不必要的反对票,并鼓励其他人这样做。不仅很清楚这里要问什么,而且在 SO 中还有其他问题,比如这个。如果它是重复的,请将其标记为这样。但我们都应该停止仇恨,如果可以的话,只回答问题。
  • @JustSid: cu·ri·os·i·ty:学习或了解更多关于某事或某人的愿望(Merriam-Webster 在线词典)

标签: c++ c arrays performance


【解决方案1】:

抱歉,回答太长了。

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

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

现在回到数组。首先,在给定的示例中,应该使用memset,而不是重新发明轮子。我知道这是出于测试目的,但在这种情况下,最好使用例如rand()(虽然应该降低值,因为 rand 比 =0 慢得多,所以测试时间太长)。不过没关系,就这样吧:

在 3 维版本中,最里面的循环访问线性数组。这是非常缓存友好且快速的方式。不是在每次循环迭代时都执行取消引用,因为编译器看到它无法更改。因此,最常用的代码行 - 最内层循环 - 访问线性内存数组。

一维数组的“快速”版本也是如此。一个也不错。 memset 还是更好,不过:-)。

但是当谈到“慢”一维版本时,事情就变得一团糟了。查看您的索引行:array1d[x + SIZE_X * y + SIZE_XY * z] = 0;。最内层循环迭代z,因此在每次迭代中,您都设置了 veeeeeery far int。这种访问模式只会使数据缓存变得无用,并且大多数时候您的程序只是等待数据写入内存。但是,如果你把它改成array1d[SIZE_XY * x + SIZE_X * y + z] = 0;,它又变成了线性数组访问,因此变得非常快。另外,如果您愿意,可以在外循环中计算加法的左侧部分,这可能会使其更快一些。

但一维数组的真正伟大之处在于它可以从头到尾线性访问。如果使用它的算法可以重新排列以以这种方式遍历数组 - 这是双赢的局面。

如果您想对其进行测试,只需将 3d 版本中的 [x][y][z] order 更改为 [z][y][x] 即可看到性能显着降低。

所以,关于最初的问题 - 答案是“视情况而定”。最重要的是,它取决于数据访问模式,还取决于许多其他因素,例如数组维度的实际深度、每个维度的大小、支持效果(如新建/删除)的频率等等。但是,如果您可以线性化数据访问 - 它已经很快了,但在这种情况下您不需要 3D,对吧?

(是的,我显然赞成手动计算索引的一维数组,所以算我有偏见。对不起)。

【讨论】:

  • 这很复杂。而且是真的。将 3d 数组中的 [x][y][z] 更改为 [z][y][x],耗时 32s。
【解决方案2】:

你为什么不简单地检查一下每个选项的反汇编并找出来?

当然,反汇编取决于使用的编译器,而编译器又取决于 CPU 架构及其支持的操作。

这实际上是这里最重要的陈述,因为每个选项都可能有自己的优点和缺点,具体取决于您的平台(编译器、链接器、处理器)。

因此,如果不指定底层平台,手头的一般问题可能没有决定性的答案。


下面的答案分为两种情况。

在每种情况下,它都会检查两个选项(1D 数组和 3D 数组),以使用 Microsoft Visual C++ 2010 for Pentium E5200 编译的每个选项的反汇编为例。

案例 #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  

如您所见,“数学”略有不同,但除此之外,这两个选项实际上是相同的。所以这里唯一可能影响性能的是运行时缓存,尽管我不知道这两个选项中的任何一个在这方面比另一个有明显的优势。

案例 #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 数组时,Load (mov) 操作似乎比使用 1D 数组时要多得多。所以这里的运行时性能高度依赖于每个数组在内存中的位置(RAM、L2 Cache 等)。

【讨论】:

  • 在编译期间自动(或静态)分配的数组展开为一维。我相信它是按标准执行的。除了在另一个维度遍历数组(这是不同的算法)之外,绝对没有区别。
  • @keltar:谢谢。我认为这个事实在答案中几乎已经说明了。
猜你喜欢
  • 1970-01-01
  • 2018-06-01
  • 1970-01-01
  • 2020-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-23
相关资源
最近更新 更多