【问题标题】:Efficient stable sum of ordered numbers有序数的有效稳定总和
【发布时间】:2020-03-09 07:25:38
【问题描述】:

我有一个很长的浮点正数列表(std::vector<float>,大小约为 1000)。数字按降序排列。如果我按照顺序对它们求和:

for (auto v : vec) { sum += v; }

我想我可能会遇到一些数值稳定性问题,因为接近向量末尾的sum 会比v 大得多。最简单的解决方案是以相反的顺序遍历向量。我的问题是:这种方法和前瞻案例一样有效吗?我会丢失更多缓存吗?

还有其他智能解决方案吗?

【问题讨论】:

  • 速度问题很容易回答。对其进行基准测试。
  • 速度比准确性更重要吗?
  • 不是完全重复,但非常相似的问题:sum of series using float
  • 你可能要注意负数。
  • 如果您真的很在意高精度,请查看Kahan summation

标签: c++ floating-point precision


【解决方案1】:

bench-marked您的用例和结果(见附图)指出向前或向后循环不会产生任何性能差异的方向。

您可能还想在硬件 + 编译器上进行测量。


使用 STL 执行求和与手动循环数据一样快,但更具表现力。

使用以下方法进行反向累积:

std::accumulate(rbegin(data), rend(data), 0.0f);

同时用于前向累积:

std::accumulate(begin(data), end(data), 0.0f);

【讨论】:

  • 那个网站超级酷。只是为了确定:你没有计时随机生成,对吧?
  • 不,只有state循环中的部分是定时的。
【解决方案2】:

我想我可能有一些数值稳定性问题

所以测试一下。目前你有一个假设的问题,也就是说,完全没有问题。

如果您进行测试,并且假设变成了一个实际问题,那么您应该担心实际修复它。

也就是说 - 浮点精度可能会导致问题,但您可以先确认它是否真的适用于您的数据,然后再将其优先于其他所有内容。

...我会丢失更多缓存吗?

一千个浮点数为 4Kb - 它适合现代大众市场系统的缓存(如果您有其他平台,请告诉我们它是什么)。

唯一的风险是预取器在向后迭代时不会帮助您,但您的向量当然可能已经在缓存中。除非您在完整计划的上下文中进行概要分析,否则您无法真正确定这一点,因此在您拥有完整计划之前不必担心它。

还有其他智能解决方案吗?

不要担心可能会成为问题的事情,直到它们真正成为问题。最多值得注意的是可能的问题,并构建代码,以便以后可以用精心优化的解决方案替换最简单的解决方案,而无需重新编写其他所有内容。

【讨论】:

    【解决方案3】:

    最简单的解决方案是以相反的顺序遍历向量。我的问题是:这种方法和前瞻案例一样有效吗?我会丢失更多缓存吗?

    是的,它很有效。来自硬件的分支预测和智能缓存策略已针对顺序访问进行了调整。你可以安全地积累你的向量:

    #include <numeric>
    
    auto const sum = std::accumulate(crbegin(v), crend(v), 0.f);
    

    【讨论】:

    • 您能否澄清一下:在这种情况下,“顺序访问”是指向前、向后或两者兼而有之?
    • @RuggeroTurra 我不能,除非我能找到来源,而且我现在没心情阅读 CPU 数据表。
    • @RuggeroTurra 通常顺序访问意味着转发。所有半体面的内存预取器都会捕获前向顺序访问。
    • @Toothbrush,谢谢。所以,如果我向后循环,原则上,这可能是一个性能问题
    • 原则上,至少在某些硬件上,如果整个向量已经在 L1 缓存中。
    【解决方案4】:

    为此,您可以在std::vector&lt;float&gt; vec 中使用没有任何换位的反向迭代器:

    float sum{0.f};
    for (auto rIt = vec.rbegin(); rIt!= vec.rend(); ++rIt)
    {
        sum += *rit;
    }
    

    或者使用标准算法做同样的工作:

    float sum = std::accumulate(vec.crbegin(), vec.crend(), 0.f);
    

    性能必须相同,只改变你的矢量的旁路方向

    【讨论】:

    • 如果我错了,请纠正我,但我认为这比 OP 使用的 foreach 语句更有效,因为它引入了开销。 YSC 在数值稳定性部分是正确的。
    • @sephiroth 不,任何半体面的编译器都不会真正关心您是否编写了 range-for 或迭代器。
    • 由于缓存/预取,实际性能绝对不能保证相同。 OP对此保持警惕是合理的。
    【解决方案5】:

    如果您所说的数值稳定性是指准确性,那么是的,您最终可能会遇到准确性问题。根据最大值与最小值的比率,以及您对结果准确性的要求,这可能是也可能不是问题。

    如果您确实想获得高精度,请考虑使用Kahan summation - 它使用额外的浮点数进行误差补偿。还有pairwise summation

    有关准确度和时间之间权衡的详细分析,请参阅this article

    C++17 更新:

    其他一些答案提到了std::accumulate。从 C++17 开始,有 execution policies 允许算法并行化。

    例如

    #include <vector>
    #include <execution>
    #include <iostream>
    #include <numeric>
    
    int main()
    {  
       std::vector<double> input{0.1, 0.9, 0.2, 0.8, 0.3, 0.7, 0.4, 0.6, 0.5};
    
       double reduceResult = std::reduce(std::execution::par, std::begin(input), std::end(input));
    
       std:: cout << "reduceResult " << reduceResult << '\n';
    }
    

    这应该会以非确定性舍入错误为代价更快地对大型数据集求和(我假设用户将无法确定线程分区)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-10-19
      • 2010-09-11
      • 1970-01-01
      • 2021-08-30
      • 2014-11-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多