关于最小化浮点运算中的错误,如果我在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)似乎在操作后产生更准确的结果。我想象这个,还是扩大数字有助于提高准确度?
答案 0 :(得分:8)
注意:这个答案首先对a = a - (r * b);
和float c = r * b; a = a - c;
与符合c99的编译器之间的区别进行了长时间的讨论。关于提高准确性同时避免扩展精度的目标的部分内容将在最后介绍。
如果你的C99编译器defines FLT_EVAL_METHOD
为0,那么这两个计算可以产生完全相同的结果。如果编译器将FLT_EVAL_METHOD
定义为1或2,那么对于a = a - (r * b);
,a
和r
的某些值,b
将更精确,因为所有中间计算将以扩展的精度完成({1}为值1,double
为值2)。
程序无法设置long double
,但您可以使用命令行选项来更改编译器使用浮点计算的方式,这将使其相应地更改其定义。
根据您在程序中使用FLT_EVAL_METHOD
以及编译器的此编译指示的默认值,某些复合浮点表达式可以缩小为单个指令好像中间结果是用无限精度计算的。在针对现代处理器时,您的示例可能会出现这种情况,因为fused-multiply-add instruction将直接计算#pragma fp_contract
,并且与浮点类型允许的情况一样准确。
但是,您应该记住,收缩只发生在编译器的选项中,没有任何保证。编译器使用FMA指令来优化速度,而不是精度,因此转换可能不会在较低的优化级别进行。有时可以进行多次转换(例如a
可以计算为a * b + c * d
或fmaf(c, d, a*b)
),编译器可以选择其中一种。
简而言之,浮点计算的收缩并不是为了帮助您实现准确性。如果您希望获得可重现的结果,也可以确保它被禁用。
但是,在fusion-multiply-add复合操作的特定情况下,您可以使用C99标准函数fmaf(a, b, c*d)
告诉编译器通过单个舍入在一个步骤中计算乘法和加法。如果这样做,那么编译器将不允许产生除fmaf()
的最佳结果之外的任何其他内容。
float fmaf(float x, float y, float z); DESCRIPTION The fma() functions compute (x*y)+z, rounded as one ternary operation: they compute the value (as if) to infinite precision and round once to the result format, according to the current rounding mode.
请注意,如果FMA指令不可用,则编译器对函数a
的实现最多只需just use higher precision,如果在编译平台上发生这种情况,那么您可能就像好好使用累加器类型fmaf()
:它比使用double
更快,更准确。在最糟糕的情况下,将提供fmaf()
的有缺陷的实施。
如果您的计算涉及长链添加,请使用Kahan summation。通过简单地将fmaf()
项计算为单精度产品,可以获得一些准确性,假设它们有很多。如果您希望获得更高的准确度,您可能希望将r*b
本身计算为两个单精度数字的总和,但如果您这样做,您也可以完全切换到双单数算术。双单算法与简洁描述here的双重双重技术相同,但改为使用单精度数字。