【问题标题】:Encapsulated 2-dimensional array versus plain version - access speed封装二维数组与普通版本 - 访问速度
【发布时间】:2014-01-11 21:03:40
【问题描述】:

这个问题是这个问题的继续:2-dimensional array on heap, which version is faster?

我已经定义了类似的东西:

class Array
{
    double *data;
    int X;
    int Y;

public:
    Array(int X, int Y, double init = 0) : X(X), Y(Y)
    {
        data = new double [X*Y];
        for (int i=0; i<X*Y; i++)
            data[i] = init;
    }
    ~Array() { delete[] data; }
    double *operator[] (int x) { return (data+x*Y); }
};

我希望获得具有二维可读性的连续数组的速度优势。我以为class Array 会这样做,与

Array arr(1000,1000);
arr[x][y] = n;

与普通版本(几乎)一样快

double *arr = new double [1000*1000];
arr[x*1000+y] = n;

因为operator[] 被定义为inline

但纯版本要快得多,而封装版本仅比真正的二维版本快一点double **arr; ...; arr[x][y] = n; 不是真的,请参阅 Edit2

这正常吗?我正在 VC++ 2010 上编译,并进行了优化。

请不要使用 vector 回答,我知道这种可能性,但我对这种行为的更深层原因感兴趣......

编辑

我已经阅读了 cmets,我的 class Array 进行了 2 次查找,我应该使用直接 1 次查找并返回对 double 的引用。我试过了,没有速度提升,完全一样。

而且我真的不明白为什么我的班级会进行 2 次查找:

Array arr(1000,1000);
arr[x][y] = n;

应该内联到:

(arr.data+x*arr.Y)[y] = n;

还有:

*((arr.data+x*arr.Y)+y) = n;

什么是完全一样的:

arr.data[x*arr.Y+y] = n; // the proposed 1 lookup access

我错了吗?

编辑2

我再次计时并注意到,double **arr; arr[x][y] = n; 解决方案有不同的时间,从 1:47 分钟到 2:10 分钟 - 随机样式。

所有其他解决方案:

  1. 像上面那样封装class Array
  2. double &amp;operator() (int x, int y) 喜欢建议
  3. 与普通double *arr; arr[x*Y+y] = n;

实际上相同在 1:44 分钟左右快速并且始终保持不变。

【问题讨论】:

  • "... 比真正的二维更快一点" :您对此的评价完全相反。指向动态数据数组的指针的动态数组是“真正的二维数组”。它与 real 二维数组 Type data[N][M] 的唯一共同点是语法糖,生成的操作码的反汇编将向您展示这一点。您是正确的,具有单次查找的连续区域会更快;您的代码根本没有这样做(还)。我相信一些答案会证明这一点。
  • 关于您对扩展的编辑是否相同:实际上是的,您设置它的方式应该是,因为您重载会改变括号的含义。因此,确实您的类数组解决方案应该与所有单查找解决方案一样快,而双 ** 方式的速度比您发现的要慢。可能值得查看您的分析测试,好像您没有对数组做太多事情,那么编译器可能会优化大部分测试,导致结果不准确(或者如果没有进行优化,那么测试是无用的对于现实世界)。

标签: c++ arrays optimization heap-memory


【解决方案1】:

您没有获得性能优势,因为您仍然需要在包装版本中进行两次内存查找。仅访问元素 x*1000+y 的一维情况下的算术运算只需要一次内存查找。您的包装版本返回一个指针,然后必须再次取消引用,这是缓慢的部分。

尝试将您的包装版本访问权限重铸为

inline double  operator()(int x, int y) const {return data[x*Y + y];}
inline double& operator()(int x, int y) {return data[x*Y + y];}

并调用为

arr(x,y) = n;

我很惊讶封装的数组比普通的二维数组快,因为它只会有更多的开销。

编辑:现在更多地研究这个问题,我发现您的解决方案实际上并没有进行两次查找,因为您的重载 [] 运算符的行为不同。请参阅我对原始帖子的评论。

【讨论】:

  • C++ 不允许仅通过返回类型来区分重载。第一个应该是const 成员来解决这个问题。
【解决方案2】:

plain 的另一个好处是维度是 const,如果您不需要运行时维度,请尝试使用模板:

也不必担心内存管理 - 这将更适合使用 std::unique_ptr 而不是 double[] 使用 std::array

template <class T, size_t X, size_t Y>
class Array
{
    using custom_array=std::array<T,X*Y>;
    std::unique_ptr<custom_array> data;

public:
    Array() : data{new custom_array} {}
    Array(const Array& rhs) : data{new custom_array(*(rhs.data))} {}
    Array& operator=(const Array& rhs) {
         if (&rhs != this)
         {
            *data=*(rhs.data);
         }
         return *this;
    }
    ~Array() {}
    T& operator() (int x, int y) { return data->at(x*Y+y); }
    T operator() (int x, int y) const { return data->at(x*Y+y); }
};

用法:Array&lt;double,1000,1000&gt; A; double b=A(3,4);

对于苹果到苹果 - 将数据分配为 std::array&lt;double,X*Y&gt;,但由于您需要堆分配,请使用上面的 unique_ptr

【讨论】:

    【解决方案3】:

    如果我理解正确,您问为什么使用 2D 与 1D 的速度似乎可以忽略不计。

    在我看来,进行 2D 矩阵访问的最佳方法是使用类似以下的方法。

    double& operator()(const int row, const int col) inline{
        return data[X*row + col];
    }
    
    double operator()(const int row, const int col) inline const{
        return data[X*row + col];
    }
    

    这为您提供了参考和复制方法。

    速度的问题在于它高度依赖于您机器的底层架构。

    第一个问题是缓存大小。显然,缓存越大越好,1D 版本应该比 2D 更好,因为连续内存通常可以更好地使用缓存。

    此外,在您的示例中,第一次访问单个元素会很慢,无论内存是如何排序的,因为该元素不在缓存中。但是,如果您多次访问该元素,或者同一区域(缓存行)中的元素,则加速应该更加明显。

    第二个问题是矢量化。根据您正在执行的操作,特别是如果它们是像加法等数学运算,它们将决定速度。如果您有带有 SSE 或 AVX 扩展的较新处理器,请确保编译器正在编译以使用这些功能,这通常在您设置优化时自动完成。您可能希望通过添加 -march=native 和 -msse3 或 Windows 等效项来确保。

    另一个小的优化是使 X,Y 为常量。这将使内联更加有效,但显然伴随着分配变得痛苦的缺点。

    最后一句话:简介,看看你在哪里花费的时间最多并加以改进。

    【讨论】:

    • 速度变慢肯定是由于二维数组需要进行两次内存查找,这比一维数组 1(加法和乘法)和 1 查找慢得多。您在此处提到的这种扁平数组方法(我显然在 7 秒后提到)既提供了封装,又加快了使用算术的速度。
    • @user2711915 我不确定我是否遵循 [x][y] 需要 2 次内存查找? data[5] 就指针而言只是 data + sizeof(data[0])*5 的语法糖如果你用 [x][y] 扩展它,则不会有额外的内存访问,因为 sizeof(data[0]) 应该在编译时知道。它只是相同的算术。因此 [x][y] 与我定义的相同,但是编译器在幕后生成。
    • data[5] 是内存查找。在另一种情况下,您转到 data[x],找到其中包含的内存地址 X,然后再进行一次查找以转到 X[y]。在查看 data[x] 中包含的值之前,您不知道 X 在哪里。如果 data 是指向常量指针数据的常量指针(它从来不是),那么原则上你可以让编译器为你做这个算术,但通常它必须是运行时查找。
    • @user2711915 好的,是的。我忘记了 data[5] 本身是一个指针,在 2D 情况下必须取消引用。
    • @en4bz:关于如何访问数组的好评论:在我的例子中,它是一个图像处理(傅里叶变换和卷积)。所以它的数学和相对相同的领域。但请参阅我的编辑。
    猜你喜欢
    • 1970-01-01
    • 2013-09-13
    • 1970-01-01
    • 1970-01-01
    • 2012-02-07
    • 2010-09-25
    • 1970-01-01
    • 1970-01-01
    • 2020-11-11
    相关资源
    最近更新 更多