【问题标题】:Why is std::accumulate so slow? [duplicate]为什么 std::accumulate 这么慢? [复制]
【发布时间】:2014-02-10 18:47:42
【问题描述】:

我正在尝试使用简单的 for 循环、std::accumulate 和手动展开的 for 循环对数组元素求和。正如我所料,手动展开循环是最快的循环,但更有趣的是 std::accumulate 比简单循环慢得多。 这是我的代码,我使用带有 -O3 标志的 gcc 4.7 编译它。 Visual Studio 将需要不同的 rdtsc 函数实现。

#include <iostream>
#include <algorithm>
#include <numeric>
#include <stdint.h>


using namespace std;

__inline__ uint64_t rdtsc() {
  uint64_t a, d;
  __asm__ volatile ("rdtsc" : "=a" (a), "=d" (d));
  return (d<<32) | a;
}

class mytimer
{
 public:
  mytimer() { _start_time = rdtsc(); }
  void   restart() { _start_time = rdtsc(); }
  uint64_t elapsed() const
  { return  rdtsc() - _start_time; }

 private:
  uint64_t _start_time;
}; // timer

int main()
{
    const int num_samples = 1000;
    float* samples = new float[num_samples];
    mytimer timer;
    for (int i = 0; i < num_samples; i++) {
        samples[i] = 1.f;
    }
    double result = timer.elapsed();
    std::cout << "rewrite of " << (num_samples*sizeof(float)/(1024*1024)) << " Mb takes " << result << std::endl;

    timer.restart();
    float sum = 0;
    for (int i = 0; i < num_samples; i++) {
        sum += samples[i];
    }
    result = timer.elapsed();
    std::cout << "naive:\t\t" << result << ", sum = " << sum << std::endl;

    timer.restart();
    float* end = samples + num_samples;
    sum = 0;
    for(float* i = samples; i < end; i++) {
        sum += *i;
    }
    result = timer.elapsed();
    std::cout << "pointers:\t\t" << result << ", sum = " << sum << std::endl;

    timer.restart();
    sum = 0;
    sum = std::accumulate(samples, end, 0);
    result = timer.elapsed();
    std::cout << "algorithm:\t" << result << ", sum = " << sum << std::endl;

    // With ILP
    timer.restart();
    float sum0 = 0, sum1 = 0;
    sum = 0;
    for (int i = 0; i < num_samples; i+=2) {
        sum0 += samples[i];
        sum1 += samples[i+1];
    }
    sum = sum0 + sum1;
    result = timer.elapsed();
    std::cout << "ILP:\t\t" << result << ", sum = " << sum << std::endl;
}

【问题讨论】:

标签: c++ performance optimization


【解决方案1】:

首先,您对std::accumulate 的使用是对整数求和。 因此,您可能要支付转换每个 在添加之前将浮点数转换为整数。试试:

sum = std::accumulate( samples, end, 0.f );

看看有没有区别。

【讨论】:

  • @stefan 是的,如果他使用的是float。 (我会避免使用float 来对任何相当多的事物求和。就此而言,如果元素足够多,即使使用doublestd::accumulate 也是不准确的。当然,他的循环也是如此。)跨度>
  • 就是这样。将0 替换为0.0F 后,结果将变得相同,给出或接受统计错误(我为此demo on ideone 使用了慢速计时器)。
  • 如果将迭代次数增加到 100 万次,accumulate is faster
  • @Praetorian 两者都不是很准确。您不能天真地使用 float 对一百万个值求和,并期望得到接近正确结果的任何地方。
  • 我已将 0 更改为 0.0f 并且 std::accumulate 开始像简单循环一样快速工作。很好的答案!
【解决方案2】:

由于您(显然)关心如何快速完成此操作,因此您也可以考虑尝试多线程计算以利用所有可用内核。我对使用 OpenMP 的幼稚循环进行了非常简单的重写,给出了这个:

timer.restart();
sum = 0;

// only real change is adding the following line:
#pragma omp parallel for schedule(dynamic, 4096), reduction(+:sum)
for (int i = 0; i < num_samples; i++) {
    sum += samples[i];
}
result = timer.elapsed();
std::cout << "OMP:\t\t" << result << ", sum = " << sum << std::endl;

只是为了微笑,我还对您的展开循环进行了一些重写,以允许半任意展开,并添加 OpenMP:

static const int unroll = 32;
real total = real();
timer.restart();
double sum[unroll] = { 0.0f };
#pragma omp parallel for reduction(+:total) schedule(dynamic, 4096)
for (int i = 0; i < num_samples; i += unroll) {
    for (int j = 0; j < unroll; j++)
        total += samples[i + j];
}
result = timer.elapsed();
std::cout << "ILP+OMP:\t" << result << ", sum = " << total << std::endl;

我还增加了数组大小(基本上)以获得更有意义的数字。结果如下。首先是双核 AMD:

rewrite of 4096 Mb takes 8269023193
naive:          3336194526, sum = 536870912
pointers:       3348790101, sum = 536870912
algorithm:      3293786903, sum = 536870912
ILP:            2713824079, sum = 536870912
OMP:            1885895124, sum = 536870912
ILP+OMP:        1618134382, sum = 536870912

那么对于四核(Intel i7):

rewrite of 4096 Mb takes 2415836465
naive:          1382962075, sum = 536870912
pointers:       1675826109, sum = 536870912
algorithm:      1748990122, sum = 536870912
ILP:            751649497, sum = 536870912
OMP:            575595251, sum = 536870912
ILP+OMP:        450832023, sum = 536870912

从表面上看,OpenMP 版本可能在内存带宽上遇到了限制——OpenMP 版本比非线程版本更多地使用 CPU,但仍然只有 70% 左右,这表明有些除了 CPU 之外,其他都是瓶颈。

【讨论】:

  • 干得好!顺便说一句,使用 TSC 寄存器的时序不是这个任务的好选择。它将计算在同一 CPU 上安排的上下文切换和其他任务,而不仅仅是您尝试测量的线程。比较适合单线程运行的小段代码。
  • 我不明白你的展开代码。您定义了 double sum[unroll] 但从不使用它。在您的循环中,您只有一个累加器 (total) 而不是多个。编译器不会为您执行此操作。您必须定义与浮点加法的延迟和吞吐量相等的累加器数量。在 SB/IB 上是 3 个累加器,在 Haswell 上是 10 个。stackoverflow.com/questions/21090873/…
  • @Zboson:很抱歉让您感到困惑。我编写了一个使用sum[unroll] 的代码版本,但随后在没有它的情况下重写了它。 OpenMP 可以为您定义单独的累加器——reduction(+:total) 表示它为每个线程分配一个单独的“总数”,然后在最后将它们相加。
  • @JerryCoffin,OpenMP 在缩减中所做的是为每个线程创建一个单独的累加器,而不是为每个内核创建多个累加器。那不是展开。您仍然需要在 SB/IB 上使用 OpenMP 进行三个缩减才能获得最佳结果。
  • @Zboson:正如我所说,我曾经使用过多个累加器(这就是定义在那里结束的方式)。使用它后代码并没有明显加快,因此简化代码似乎是正确的做法。
猜你喜欢
  • 2014-11-23
  • 2014-03-01
  • 1970-01-01
  • 2014-10-26
  • 2020-02-01
  • 2010-11-01
  • 1970-01-01
  • 2015-03-15
  • 2022-12-15
相关资源
最近更新 更多