应该添加浮点数以获得最精确的结果?

Yip*_*Yay 105 c++ floating-point precision

这是我在最近的采访中被问到的一个问题,我想知道(我实际上并不记得数值分析的理论,所以请帮助我:)

如果我们有一些累积浮点数的函数:

std::accumulate(v.begin(), v.end(), 0.0);
Run Code Online (Sandbox Code Playgroud)

vstd::vector<float>例如,是一个.

  • 在累积它们之前对这些数字进行排序会更好吗?

  • 哪个订单会给出最准确的答案?

我怀疑按升序排序数字实际上会减少数值误差,但不幸的是我无法证明这一点.

PS我确实意识到这可能与现实世界的编程无关,只是好奇.

Ste*_*sop 108

你的直觉基本上是正确的,按升序(大小)排序通常会有所改善.考虑我们添加单精度(32位)浮点数的情况,并且有10亿个值等于1 /(10亿),并且一个值等于1.如果1首先出现,那么总和将会到来由于精度损失,1 +(1/1十亿)为1.每次添加对总数都没有影响.

如果小值首先出现,它们至少会总结为某些东西,尽管即便如此,我有2 ^ 30个,而在2 ^ 25左右之后,我又回到了每个人不会影响总数的情况下更多.所以我仍然需要更多技巧.

这是一个极端情况,但一般情况下,添加两个相似幅度的值比添加两个非常不同的值的值更准确,因为您以较小的值"丢弃"较少的精度位.通过对数字进行排序,您可以将相似幅度的值组合在一起,并通过按升序添加它们,您可以为小值提供累积达到较大数字幅度的"机会".

尽管如此,如果涉及负数,很容易"智胜"这种方法.考虑三个要求的值,{1, -1, 1 billionth}.算术正确的总和是1 billionth,但如果我的第一个加法涉及微小的值,那么我的最终总和将是0.在6个可能的订单中,只有2个是"正确的" - {1, -1, 1 billionth}和{-1, 1, 1 billionth}.所有6个订单给出的结果在输入中最大幅度值的范围内是准确的(0.0000001%输出),但是对于其中4个,结果在真实解决方案的范围内是不准确的(100%输出).您正在解决的特殊问题将告诉您前者是否足够好.

事实上,除了按排序顺序添加它们之外,您还可以玩更多技巧.如果你有很多非常小的值,中等数量的中等值,以及少量的大值,那么首先将所有小值相加可能是最准确的,然后分别总计中等值,加上这两个总数然后加上大的.找到最准确的浮点加法组合并不是一件容易的事情,但是为了应对非常糟糕的情况,你可以保持不同大小的整个运行总数,将每个新值添加到与其幅度最匹配的总数中,当一个运行总计开始变得太大而不是它的大小时,将它添加到下一个总计并开始一个新的.从它的逻辑极端来看,这个过程相当于以任意精度类型执行求和(所以你要这样做).但是考虑到以递增或递减的顺序添加的简单选择,升序是更好的选择.

它确实与现实世界的编程有一定关系,因为在某些情况下,如果你不小心砍掉由大量值组成的"重"尾巴,你的计算可能会出现严重错误,每个值都太小而不能单独影响总和,或者如果你从许多小的值中抛弃太多的精度,这些小的值只会影响总和的最后几位.如果尾巴可以忽略不计,你可能不在乎.例如,如果您只是首先将少量值加在一起,而您只使用总和中的一些有效数字.

  • +1解释.这有点违反直觉,因为加法通常在数值上是稳定的(与减法和除法不同). (8认同)
  • 如果你要坚持使用这个简单的方案,我总是会添加最低幅度的两个数字,然后重新插入集合中的和.(好吧,合并排序在这里可能效果最好.您可以使用包含先前求和的数字的数组部分作为部分总和的工作区域.) (8认同)
  • @ 6502:它们按数量级排序,因此-1结尾.如果总数的真实值为1,那就没问题了.如果你将三个值加在一起:1 /十亿,1和-1,那么,你得到0,此时你必须回答有趣的实际问题 - 你需要一个在真正的总和,或者你只需​​要一个在最大值的范围内准确的答案?对于某些实际应用,后者足够好,但如果不是,则需要更复杂的方法.量子物理学使用重整化. (3认同)
  • @Konrad,它可能在数值上是稳定的,但鉴于不同大小的操作数,它并不精确:) (2认同)
  • @Kevin Panko:简单版本是单精度浮点数有24个二进制数字,其中最大的是数字中最大的设置位.因此,如果将两个幅度相差超过2 ^ 24的数字加在一起,则会损失较小的值,如果它们的幅度差异较小,则会丢失相应数量的较小精度位数.数. (2认同)
  • 只是想到了一些事情:而不是按数量级排序,只需对数字进行排序.然后将数字拆分为0:从0开始添加正数,从0开始添加负数(即向后),然后添加您获得的两个结果.这避免了减去两个几乎相等的数字(或者添加几乎相同幅度的正值和负值)并获得非常小的值作为中间结果.IOW,你按照数量级对数字进行排序的方式使得两个相等幅度但不同符号的数字相加得更大. (2认同)

Dan*_*den 88

还有一种算法设计用于这种累积操作,称为Kahan Summation,你可能应该知道.

根据维基百科,

的Kahan的求和算法(也称为补偿求和)显著降低通过添加有限精度浮点数的一个序列获得的总的数值误差,相对于明显的方法.这是通过保持单独的运行补偿(一个变量来累积小错误)来完成的.

在伪代码中,算法是:

function kahanSum(input)
 var sum = input[1]
 var c = 0.0          //A running compensation for lost low-order bits.
 for i = 2 to input.length
  y = input[i] - c    //So far, so good: c is zero.
  t = sum + y         //Alas, sum is big, y small, so low-order digits of y are lost.
  c = (t - sum) - y   //(t - sum) recovers the high-order part of y; subtracting y recovers -(low part of y)
  sum = t             //Algebraically, c should always be zero. Beware eagerly optimising compilers!
 next i               //Next time around, the lost low part will be added to y in a fresh attempt.
return sum
Run Code Online (Sandbox Code Playgroud)

  • @Konrad Rudolph感谢你指出这可能是`-ffast-math`的优化.我从这次讨论和[这个链接](http://gcc.gnu.org/ml/gcc/2001-07/msg02150.html)中学到的是,如果你关心数值精度,你应该避免使用` - ffast-math`但是在许多应用程序中你可能受CPU约束但不关心精确的数值计算(例如游戏编程),`-ffast-math`是合理的.因此,我想修改我措辞强硬的"禁止"评论. (6认同)
  • +1这个主题的可爱添加.应该禁止任何"急切优化"这些语句的编译器. (3认同)
  • @ChrisA.你可以在所有计算的编译器上明确地控制它(例如通过GCC上的`-ffast-math`). (2认同)

And*_*ein 34

我在Steve Jessop提供的答案中尝试了极端的例子.

#include <iostream>
#include <iomanip>
#include <cmath>

int main()
{
    long billion = 1000000000;
    double big = 1.0;
    double small = 1e-9;
    double expected = 2.0;

    double sum = big;
    for (long i = 0; i < billion; ++i)
        sum += small;
    std::cout << std::scientific << std::setprecision(1) << big << " + " << billion << " * " << small << " = " <<
        std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    sum = 0;
    for (long i = 0; i < billion; ++i)
        sum += small;
    sum += big;
    std::cout  << std::scientific << std::setprecision(1) << billion << " * " << small << " + " << big << " = " <<
        std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我得到了以下结果:

1.0e+00 + 1000000000 * 1.0e-09 = 2.000000082740371    (difference = 0.000000082740371)
1000000000 * 1.0e-09 + 1.0e+00 = 1.999999992539933    (difference = 0.000000007460067)
Run Code Online (Sandbox Code Playgroud)

第一行中的错误在第二行中大于十倍.

如果我在上面的代码中将doubles 更改为s float,我得到:

1.0e+00 + 1000000000 * 1.0e-09 = 1.000000000000000    (difference = 1.000000000000000)
1000000000 * 1.0e-09 + 1.0e+00 = 1.031250000000000    (difference = 0.968750000000000)
Run Code Online (Sandbox Code Playgroud)

答案都不接近2.0(但第二个稍微接近).

使用doubleDaniel Pryden描述的Kahan求和(用s):

#include <iostream>
#include <iomanip>
#include <cmath>

int main()
{
    long billion = 1000000000;
    double big = 1.0;
    double small = 1e-9;
    double expected = 2.0;

    double sum = big;
    double c = 0.0;
    for (long i = 0; i < billion; ++i) {
        double y = small - c;
        double t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }

    std::cout << "Kahan sum  = " << std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我得到2.0:

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)
Run Code Online (Sandbox Code Playgroud)

即使我在上面的代码中将doubles 更改为s float,我得到:

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)
Run Code Online (Sandbox Code Playgroud)

似乎Kahan是要走的路!

  • Kahan仍然具有有限的精度,但是要构造一个杀手级的情况,你需要主和和误差累加器`c`包含比下一个加数大得多的值.这意味着总和远远小于主要总和,所以必须有很多它们加起来很多.特别是使用`double`算术. (4认同)

NPE*_*NPE 14

有一类算法可以解决这个确切的问题,而无需对数据进行排序或重新排序.

换句话说,求和可以在数据上一次完成.这也使得这样的算法适用于预先不知道数据集的情况,例如,如果数据实时到达并且需要维持运行总和.

这是最近一篇论文的摘要:

我们提出了一种新颖的在线算法,用于精确求和浮点数流."在线"是指算法一次只能看到一个输入,并且可以采用这种输入的任意长度输入流,同时只需要恒定的存储器."精确"是指我们算法的内部数组的总和恰好等于所有输入的总和,返回的结果是正确舍入的总和.正确性证明对所有输入(包括非标准化数字但模数中间溢出)有效,并且与加数的数量或和的条件数无关.该算法渐近地每个求和只需要5个FLOP,并且当指令级数大于10,000时,由于指令级并行性运行速度仅比明显的,快速但愚蠢的"普通递归求和"循环慢约2-3倍. .因此,据我们所知,它是已知算法中最快,最准确,最大的内存效率.实际上,如果没有硬件改进,很难看出更快的算法或需要明显更少FLOP的算法.提供了大量加数的应用程序.

来源:算法908:浮点流的在线精确求和.