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

Ed *_*ing 3 c floating-point 32-bit floating-accuracy

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

float a = 123.456;
float b = 456.789;
float r = 0.12345;
a = a - (r * b);
Run Code Online (Sandbox Code Playgroud)

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

float c = r * b;
a = a - c;
Run Code Online (Sandbox Code Playgroud)

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

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

请不要只说"使用更高的精度" - 这不是我所追求的.

编辑

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

Pas*_*uoq 8

注意:这个答案与之间的区别了长时间的讨论开始a = a - (r * b);,并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因为所有中间计算将以扩展精度(double对于值1和long double值2)完成.

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

收缩一些中间结果

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

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

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

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


     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指令不可用,那么编译器的函数实现fmaf()最多只能使用更高的精度,如果在编译平台上发生这种情况,你可能也会使用double累加器的类型:它会更快,比使用更准确fmaf().在最坏的情况下,fmaf()将提供有缺陷的实施.

仅使用单精度提高精度

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