内存布局:2D N * M数据作为指向N * M缓冲区的指针或作为指向数组的N指针数组

时间:2014-08-07 01:31:23

标签: c++ c arrays pointers data-structures

我对如何组织2D数据的内存布局犹豫不决。 基本上,我想要的是一个N * M 2D double数组,其中N~M是数千(并且来自用户提供的数据)

我看到它的方式,我有两个选择:

double *data = new double[N*M];

double **data = new double*[N];
for (size_t i = 0; i < N; ++i)
     data[i] = new double[M];

首选是我倾向于的。 我看到的主要优点是更短的新/删除语法,如果我正确安排访问,连续内存布局意味着在运行时相邻的内存访问,以及可能更好的矢量化代码性能(自动矢量化或使用矢量库,如vDSP或vecLib)

另一方面,在我看来,与分配一堆较小的连续内存相比,分配大量连续内存可能会失败/花费更多时间。与data[i][j]

相比,第二种方法还具有缩短语法data[i*M+j]的优势

最常见/更好的方法是什么,主要是如果我尝试从性能的角度来看待它(尽管那些会有很小的改进,我很想知道哪个更能表现)

3 个答案:

答案 0 :(得分:2)

在前两个选项之间,对于MN的合理值,我几乎肯定会选择1.您跳过指针取消引用,如果您访问数据,则可以获得良好的缓存正确的顺序。

就您对尺寸的担忧而言,我们可以做一些背后的计算。

由于MN数以千计,因此假设每个10000为上限。然后消耗的总内存是

 10000 * 10000 * sizeof(double) = 8 * 10^8

这大约是800 MB,虽然很大,但考虑到现代机器中的内存大小,这是非常合理的。

答案 1 :(得分:2)

如果NM是常量,最好将静态声明所需的内存声明为二维数组。或者,您可以使用std::array

std::array<std::array<double, M>, N> data;

如果只有M是常量,则可以使用std::vector代替std::array

std::vector<std::array<double, M>> data(N);

如果M不是常数,则需要执行一些动态分配。但是,std::vector可用于为您管理内存,因此您可以围绕它创建一个简单的包装器。下面的包装器返回一个row中间对象,以允许第二个[]运算符实际计算vector的偏移量。

template <typename T>
class matrix {
    const size_t N;
    const size_t M;
    std::vector<T> v_;
    struct row {
        matrix &m_;
        const size_t r_;
        row (matrix &m, size_t r) : m_(m), r_(r) {}
        T & operator [] (size_t c) { return m_.v_[r_ * m_.M + c]; }
        T operator [] (size_t c) const { return m_.v_[r_ * m_.M + c]; }
    };
public:
    matrix (size_t n, size_t m) : N(n), M(m), v_(N*M) {}
    row operator [] (size_t r) { return row(*this, r); }
    const row & operator [] (size_t r) const { return row(*this, r); }
};


matrix<double> data(10,20);
data[1][2] = .5;
std::cout << data[1][2] << '\n';

解决您对性能的特殊关注:您希望单个内存访问的理由是正确的。你应该避免自己做newdelete(这是这个包装器提供的东西),如果数据更自然地被解释为多维的,那么在代码中会显示代码也更容易阅读。

你的第二种技术中显示的多次分配是次要的,因为它需要更多的时间,但它的优点是,如果你的系统碎片化,它可能会更频繁地成功(可用内存由较小的孔组成,而你没有免费的大块内存,足以满足单个分配请求)。但是多个分配还有另一个缺点,就是需要更多的内存来为每行的指针分配空间。

我的建议提供单一分配技术,无需明确调用newdelete,因为内存由vector管理。同时,它允许使用二维语法[x][y]来处理数据。因此,只要您有足够的内存来满足分配请求,它就可以提供单一分配的所有好处以及多分配的所有好处。

答案 2 :(得分:0)

考虑使用以下内容:

// array of pointers to doubles to point the beginning of rows
double ** data = new double*[N];

// allocate so many doubles to the first row, that it is long enough to feed them all
data[0] = new double[N * M];

// distribute pointers to individual rows as well
for (size_t i = 1; i < N; i++)
    data[i] = data[0] + i * M;

我不确定这是否是一般做法,我只想出了这个。一些不足仍然适用于这种方法,但我认为它消除了大多数,例如能够访问像data[i][j]这样的个人双打。