【问题标题】:C++: Is accessing values in pairs so much more efficient than accessing array elements?C++:成对访问值是否比访问数组元素更有效?
【发布时间】:2022-01-08 18:06:25
【问题描述】:

假设我们有一个双精度数组x 和一个索引数组y,并且我们希望通过x 中的相应值对这些索引进行排序(因此,将[i in y] 排序为x[i] 作为键)。

然后我们可以创建一个对数组,其中一个组件是键值,一个是索引,例如,执行以下操作:

boost::sort::spreadsort::float_sort(sortdata, sortdata + n,
    [](const std::pair<double, int> &a, const unsigned offset) -> boost::int64_t {
        return boost::sort::spreadsort::float_mem_cast<double, boost::int64_t>(a.first) >> offset;
    },
    [](const std::pair<double, int> &a, const std::pair<double, int> &b) -> bool {
        return a.first < b.first;
    });

这需要相当多的内存,所以我们可以省略创建这个对数组,直接使用这样的数据:

boost::sort::spreadsort::float_sort(y, y + n,
    [x](const int a, const unsigned offset) -> boost::int64_t {
        return boost::sort::spreadsort::float_mem_cast<double, boost::int64_t>(x[a]) >> offset;
    },
    [x](const int a, const int b) -> bool {
        return x[a] < x[b];
    });

现在,当在非常大的数据(比如 50000000 个条目)上使用它时,第二种方法所需的时间是第一种方法的两倍多。据我所知,std::pair 只是struct。那么,数组访问是否真的比结构访问效率低得多?或者,我在这里做错了什么?

这是一个完整的比较示例:

#include <cstdlib>
#include <ctime>
#include <boost/sort/spreadsort/spreadsort.hpp>
#include <iostream>

int main() {
    int n = 50000000;
    double *x = new double[n];
    int *y = new int[n];
    std::pair<double, int> *sortdata = new std::pair<double, int> [n];
    for (int i=0; i < n; i++) {
        x[i] = ((double) std::rand()) / ((double) std::rand());
        sortdata[i].first = x[i];
        y[i] = i;
        sortdata[i].second = i;
    }

    std::time_t t = std::time(0);
    boost::sort::spreadsort::float_sort(sortdata, sortdata + n,
        [](const std::pair<double, int> &a, const unsigned offset) -> boost::int64_t {
            return boost::sort::spreadsort::float_mem_cast<double, boost::int64_t>(a.first) >> offset;
        },
        [](const std::pair<double, int> &a, const std::pair<double, int> &b) -> bool {
            return a.first < b.first;
        });

    std::cout << std::time(0)-t << "\n";

    t = std::time(0);
    boost::sort::spreadsort::float_sort(y, y + n,
        [x](const int a, const unsigned offset) -> boost::int64_t {
            return boost::sort::spreadsort::float_mem_cast<double, boost::int64_t>(x[a]) >> offset;
        },
        [x](const int a, const int b) -> bool {
            return x[a] < x[b];
        });

    std::cout << std::time(0)-t << "\n";
}

在我的系统上使用g++ -O2 运行此程序会在第一时间为 3 秒,为最后一个时间为 9 秒。

【问题讨论】:

  • 如果没有完整的minimal reproducible example,不可能有权威的答案,但在我看来,第一种情况直接比较值,而第二种情况有间接比较。这实际上会产生相当多的开销。
  • 您记得启用优化吗?
  • @OP 任何有关 C++ 代码性能的问题都必须附带您用于构建程序的编译器选项。如果您正在运行“调试”或未优化的构建,那么您向我们展示的时间信息是没有意义的。
  • 显然a.first &lt; b.first 看起来比x[a] &lt; x[b] 快,无论x 是什么类型——vector/set/map/etc 取消引用都会比访问已经可用的值慢,而不是提到 x 被按值捕获(副本)。
  • 问题不在于取消引用值的成本,也不是结构与数组的成本,而是内存访问模式。并且对于输入数组,第二种方法可以比前者更快或更慢。如果数组包含随机值,前者将比第二个(执行不可预测的内存随机访问)快得多。对于像您的示例中那样不适合缓存的大数组尤其如此。请注意,排序算法也很重要(因为比较与副本的数量会发生变化)。

标签: c++ arrays performance struct std-pair


【解决方案1】:

不是专家,但我认为是这样的:

无论使用哪种编程语言,在内存中跳转都是一项代价高昂的操作。这与 CPU 的缓存架构有关。

数据成对交错存储在内存中。没有类边界或类似的东西。它看起来像这样:

value0 index0 value1 index1 value2 index2 ...

现在,当您将数据存储在两个数组中时,您的数据将如下所示:

value0 value1 value2 .........  index0 index1 index2 .....

好的,现在让我们看看当排序算法试图确定索引 46 处的数据应该排在索引 47 处的数据之前还是之后时会发生什么:

在成对的情况下:

1. ask ram for value46 (expensive!)
   because of how cachelines work ~ 64bytes of data will be pulled. 
   that is: value46 index46 value47 index47 are pulled, maybe a bit more.
2. ask ram for value47 (cheap, it's already cached)

两个数组的情况:

1. ask ram for index46 (expensive)
   this will pull index46,index47,...
2. ask ram for index47 (cheap)
3. ask ram for x[index46] (expensive)
   this will pull x[index46],x[index46+1...]
4. ask ram for x[index47] (should be next to x[index46]? not sure... could be cheap, could be expensive)

嗯,我不确定最后一次内存获取,但关键是你在内存中跳跃的次数大约是 2 到 3 倍,我很确定这就是为什么你要粗略测量运行时间的两倍。


下面是一个说明这一点的最后一个例子:

int N = 100000; 
float data[N] = {... random data...};
int idx_1[N] = {0,1,....};
int idx_2[N] = {... random permutation of 0,...N-1};

// this will be very fast. 
// the cache predictor always gets it right. 
float sum_1 = 0; 
for(int i = 0; i < N; i++) sum_1 += data[idx_1[i]];

// this will be very slow. 
// the cache predictor always gets it wrong. 
float sum_2 = 0; 
for(int i = 0; i < N; i++) sum_2 += data[idx_2[i]];

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-10-25
    • 2011-04-26
    • 2014-12-06
    • 2016-10-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-10
    相关资源
    最近更新 更多