最小化C中浮点错误的经验法则?

关于最小化浮点运算中的错误,如果我在C中执行如下操作:

float a = 123.456; float b = 456.789; float r = 0.12345; a = a - (r * b); 

如果我将乘法和减法步骤分开,计算结果是否会改变,即:

 float c = r * b; a = a - c; 

我想知道CPU是否会以不同方式处理这些计算,从而在一种情况下误差可能会更小?

如果不是,我认为无论如何,是否有任何良好的经验法则来缓解浮点错误? 我可以按照有用的方式按摩数据吗?

请不要只说“使用更高的精度” – 这不是我所追求的。

编辑

有关数据的信息,在一般意义上,当操作导致非常大的数字(如123456789)时,错误似乎更糟。小数字(例如1.23456789)似乎在操作后产生更准确的结果。 我想象这个,还是扩大数字有助于准确?

注意:这个答案首先是对a = a - (r * b);之间区别的冗长讨论a = a - (r * b);float c = r * b; a = a - c; float c = r * b; a = a - c; 使用符合c99标准的编译器。 最后讨论了关于提高准确性同时避免扩展精度的目标的部分问题。

中间结果的扩展浮点精度

如果您的C99编译器将 FLT_EVAL_METHOD 定义为0,那么这两个计算可以产生完全相同的结果。 如果编译器将FLT_EVAL_METHOD定义为1或2,则a = a - (r * b); 对于arb某些值,将更加精确,因为所有中间计算都将以扩展精度完成(值为1的long double ,值为2的long double )。

程序无法设置FLT_EVAL_METHOD ,但您可以使用命令行选项来更改编译器使用浮点计算的方式,这将使其相应地更改其定义。

收缩一些中间结果

根据您是否在程序中使用#pragma fp_contract以及编译器的编译器默认值,可以将一些复合浮点表达式缩减为单个指令,其行为就像中间结果是以无限精度计算的一样。 在针对现代处理器时,这恰好是您的示例的可能性,因为融合乘法 – 加法指令将直接计算浮点类型所允许的精度。

但是,您应该记住,收缩只发生在编译器的选项上,没有任何保证。 编译器使用FMA指令来优化速度,而不是精度,因此转换可能不会在较低的优化级别进行。 有时可以进行多次转换(例如a * b + c * d可以计算为fmaf(c, d, a*b)fmaf(a, b, c*d) ),编译器可以选择一个或者其他。

简而言之,浮点计算的收缩并不是为了帮助您实现准确性。 如果您喜欢可重现的结果,也可以确保它被禁用。

但是,在fmaf() -multiply-add复合操作的特定情况下,您可以使用C99标准函数fmaf()告诉编译器通过单个舍入在一个步骤中计算乘法和加法。 如果你这样做,那么编译器将不允许产生除了a的最佳结果之外的任何东西。


      float fmaf(float x,float y,float z);

描述
      fma()函数计算(x * y)+ z,舍入为一个三元运算:
     他们将值(似乎)计算为无限精度并将其舍入一次
     结果格式,根据当前的舍入模式。

注意,如果FMA指令不可用,编译器的函数fmaf()的实现最多只会使用更高的精度 ,如果在编译平台上发生这种情况,你可能也会使用类型double作为累加器:它将比使用fmaf()更快,更准确。 在最坏的情况下,将提供有缺陷的fmaf()实现。

仅使用单精度提高精度

如果您的计算涉及长链添加,请使用Kahan求和 。 通过简单地将r*b项计算为单精度产品,可以获得一些准确性,假设它们中有许多。 如果你想获得更高的准确度,你可能想要将r*b本身精确地计算为两个单精度数的总和,但如果你这样做,你也可以完全切换到双单数算术。 双单算法将与此处简洁描述的双重双重技术相同,但使用单精度数字。