块中的多维数组分配

multi dimensional array allocation in chunks

本文关键字:数组 分配      更新时间:2023-10-16

我今天在浏览多维数组时,发现了一个区分矩形数组和锯齿状数组的博客;通常我会在锯齿状和矩形上都这样做:

Object** obj = new Obj*[5];
for (int i = 0; i < 5; ++i)
{
   obj[i] = new Obj[10];
}

但在那篇博客中,有人说,如果我知道2d数组是矩形的,那么我最好把整个数组分配到1d数组中,并使用一种即兴的方式访问元素,比如这样:

Object* obj = new Obj[rows * cols];
obj[x * cols + y]; 
//which should have been obj[x][y] on the previous implementation

不知怎么的,我知道分配一个连续的内存块会很好,但我真的不明白这会有多大的不同,有人能解释吗?

首先,当您分配和释放对象时,您只需要执行一次分配/释放。

更重要的是:当你使用数组时,你基本上可以用乘法来交换内存访问。在现代计算机上,内存访问比算术慢得多。

这有点像谎言,因为大部分内存访问的缓慢性都被缓存所掩盖——经常被访问的内存区域被存储在CPU内部或非常靠近CPU的快速内存中,并且可以更快地访问。但这些缓存的大小有限,因此(1)如果您的数组不是一直在使用,那么行指针可能不在缓存中;(2)如果它一直在使用,那么它们可能会占用其他东西可能使用的空间。

不过,它的具体工作方式将取决于代码的细节。在许多情况下,它不会对程序的速度产生明显的影响。你可以同时尝试两种方法和基准测试。

[在被Peter Schneider的评论提醒后,编辑补充道:]此外,如果你分别分配每一行,它们可能最终都在内存的不同部分,这可能会使你的缓存效率降低——数据会被分块拉到缓存中,如果你经常从一行的末尾到下一行的开头,那么你会从中受益。但这是一个微妙的问题;在某些情况下,让您的行在内存中等距实际上可能会使缓存的性能变差,如果您连续分配几行,它们很可能在内存中(几乎)挨着一行,而且在任何情况下,除非您的行很短,否则这可能无关紧要。

将一个2D数组分配为一个大块允许编译器生成比在多个块中生成更高效的代码。至少,在一个块方法中会有一个指针去引用操作。BTW,像这样声明2D阵列:

Object obj[rows][cols];
obj[x][y]; 

相当于:

Object* obj = new Obj[rows * cols];
obj[x * cols + y]; 

在速度方面。但是第一个不是动态的(您需要在编译时指定"rows"answers"cols"的值。

通过拥有一大块连续的内存,您可以提高性能,因为缓存中已经有更多的内存访问。这个想法被称为缓存位置。我们说大数组具有更好的缓存位置。现代处理器有几个级别的缓存。最低级别通常是最小和最快的。

以有意义的方式访问数组仍然是值得的。例如,如果数据按行主顺序存储,而您按列主顺序访问它,那么您的内存访问就分散了。在特定的大小下,这种访问模式将抵消缓存的优势。

拥有良好的缓存性能比您可能关心的索引倍增值要好得多。

如果数组的一个维度是编译时常数,那么也可以在一个块中动态分配一个"真正的二维数组",然后以通常的方式对其进行索引。与数组的所有动态分配一样,new返回一个指向元素类型的指针。在二维数组的情况下,元素依次是数组——一维数组。生成的元素指针的语法有点麻烦,主要是因为去引用operator*()的优先级低于索引operator[]()。一个可能的分配语句可以是int (*arr7x11)[11] = new int[7][11];

下面是一个完整的例子。正如您所看到的,分配中最内部的索引可以是运行时值;它确定所分配数组中元素的个数。其他索引决定了动态分配数组的元素类型(因此决定了元素大小以及整体大小),当然,执行分配时必须知道这些元素。如上所述,元素本身是阵列,这里是11点的一维阵列。

#include<cstdio>
using namespace std;
int main(int argc, char **argv)
{
    constexpr int cols = 11;
    int rows = 7;
    // overwrite with cmd line arg if present.
    // if scanf fails, default is retained.
    if(argc >= 2) { sscanf(argv[1], "%d", &rows); }
    // The actual allocation of "rows" elements of 
    // type "array of 'cols' ints". Note the brackets
    // around *arr7x11 in order to force operator
    // evaluation order. arr7x11 is a pointer to array,
    // not an array of pointers.
    int (*arr7x11)[cols] = new int[rows][cols];
    for(int row = 0; row<rows; row++)
    {
        for(int col = 0; col<cols; col++)
        {
            arr7x11[row][col] = (row+1)*1000 + col+1;
        }
    }
    for(int row = 0; row<rows; row++)
    {
        for(int col = 0; col<cols; col++)
        {
            printf("%6d", arr7x11[row][col]);
        }
        putchar('n');
    }
    return 0;
}       

示例会话:

 g++ -std=c++14 -Wall -o 2darrdecl 2darrdecl.cpp && ./2darrdecl 3
  1001  1002  1003  1004  1005  1006  1007  1008  1009  1010  1011
  2001  2002  2003  2004  2005  2006  2007  2008  2009  2010  2011
  3001  3002  3003  3004  3005  3006  3007  3008  3009  3010  3011