最小化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);
对于a
, r
和b
某些值,将更加精确,因为所有中间计算都将以扩展精度完成(值为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
本身精确地计算为两个单精度数的总和,但如果你这样做,你也可以完全切换到双单数算术。 双单算法将与此处简洁描述的双重双重技术相同,但使用单精度数字。