浮点算术运算的精度是多少?
Posted
技术标签:
【中文标题】浮点算术运算的精度是多少?【英文标题】:What precision are floating-point arithmetic operations done in? 【发布时间】:2014-10-07 18:37:36 【问题描述】:考虑下面两个非常简单的乘法:
double result1;
long double result2;
float var1=3.1;
float var2=6.789;
double var3=87.45;
double var4=234.987;
result1=var1*var2;
result2=var3*var4;
默认情况下,乘法是否以比操作数更高的精度进行?我的意思是,在第一次乘法的情况下,它是以双精度完成的,而在 x86 架构中的第二次乘法的情况下,它是以 80 位扩展精度完成的,还是我们应该自己将表达式中的操作数转换为更高的精度,如下所示?
result1=(double)var1*(double)var2;
result2=(long double)var3*(long double)var4;
其他操作(加法、除法和余数)呢?例如,当添加两个以上的正单精度值时,如果用于保存表达式的中间结果,则使用双精度的额外有效位可以减少舍入误差。
【问题讨论】:
你应该阅读floating-point-gui.de @BasileStarynkevitch:这如何解决这个问题? 这很大程度上取决于您的编译器版本。三巨头的当前版本都使用 SSE2,因此使用 64 位精度。看看生成的机器码就知道了。您需要更好的测试代码,它是在您发布的 sn-ps 的编译时完成的。 【参考方案1】:浮点计算的精度
C++11 incorporates FLT_EVAL_METHOD
的定义来自 C99 中的 cfloat
。
如果您的编译器将FLT_EVAL_METHOD
定义为2,则r1
和r2
以及s1
和s2
的计算分别是等效的:
double var3 = …;
double var4 = …;
double r1 = var3 * var4;
double r2 = (long double)var3 * (long double)var4;
long double s1 = var3 * var4;
long double s2 = (long double)var3 * (long double)var4;
如果您的编译器将 FLT_EVAL_METHOD 定义为 2,那么在上述所有四个计算中,乘法运算都以 long double
类型的精度完成。
但是,如果编译器将 FLT_EVAL_METHOD
定义为 0 或 1,则 r1
和 r2
以及 s1
和 s2
并不总是相同的。计算r1
和s1
时的乘法以double
的精度完成。计算r2
和s2
时的乘法运算以long double
的精度完成。
从狭窄的论点获得广泛的结果
如果您要计算的结果注定要存储在比操作数类型更广泛的结果类型中,就像您的问题中的 result1
和 result2
一样,您应该始终将参数转换为至少一个类型与目标一样宽,就像您在此处所做的那样:
result2=(long double)var3*(long double)var4;
如果没有这种转换(如果你写var3 * var4
),如果编译器对FLT_EVAL_METHOD
的定义是0或1,那么乘积将按照double
的精度计算,这是一个耻辱,因为它注定存储在long double
中。
如果编译器将 FLT_EVAL_METHOD
定义为 2,则 (long double)var3*(long double)var4
中的转换不是必需的,但它们也不会造成伤害:表达式的含义完全相同。
题外话:如果目标格式与参数一样窄,那么中间结果的扩展精度何时更好?
矛盾的是,对于单个操作,最好只舍入一次到目标精度。以扩展精度计算单次乘法的唯一效果是将结果舍入到扩展精度,然后舍入到double
精度。这使它成为less accurate。换言之,FLT_EVAL_METHOD
0 或 1 时,由于双舍入,上述结果 r2
有时不如 r1
准确,而且如果编译器使用 IEEE 754 浮点,则永远不会更好。
对于包含多个操作的较大表达式,情况有所不同。对于这些,通常最好以扩展精度计算中间结果,要么通过显式转换,要么因为编译器使用FLT_EVAL_METHOD == 2
。这个question 及其接受的答案表明,当使用 80 位扩展精度中间计算对 binary64 IEEE 754 参数和结果进行计算时,插值公式 u2 * (1.0 - u1) + u1 * u3
总是产生u2
和u3
之间的结果u1
介于 0 和 1 之间。此属性可能不适用于 binary64 精度的中间计算,因为此时舍入误差较大。
【讨论】:
@Pooria 我很高兴您在另一个答案中找到了一些帮助,但是在计算r2
时(以及在 FLT_EVAL_METHOD 为 2 时计算 r1
时),最终结果是“多次舍入”,这称为双舍入。结果被计算并四舍五入到long double
精度(你可以反对this使用“rounded”这个词,尽管通常将IEEE 754基本操作解释为计算实际结果然后四舍五入到运算的精度),然后四舍五入到double
以便存储在内存中。
@Pooria 当你将结果赋给double
类型的变量时,它必须从FPU中的扩展格式转换为double
的格式。 C99 对此非常清楚(尽管 C++11 不太清楚)。
@Pooria:那是第二个舍入。第一次四舍五入是将乘法的“无限精确”结果四舍五入到工作精度。
@Pooria 乘法的结果是“好像”精确的结果已经被计算和四舍五入。根据内部使用的算法,这可能需要用两倍数量的有效位来表示确切的结果,或者可以使用技巧来节省空间(Garp 在他的回答中提到的三个“保护位”)。我的专长不是硬件实现,所以不能多说。但结果应该“好像”已经计算和四舍五入的确切值。
@Pooria FPU 内部发生的事情只是 FPU 设计者的问题。在讨论 C99 的网页上,“以精度 P 计算”的意思是“使用采用宽度为 P 的操作数并产生宽度为 P 的正确舍入结果的指令”,无论该指令是如何实现的(如果该操作是乘法,正如 Garp 所说,它很可能在处理器中使用更广泛的内部结果来实现,但不会存储暂时存在于 FPU 中的更广泛的结果。【参考方案2】:
浮点类型的常用算术转换在乘法、除法和取模之前应用:
对操作数执行通常的算术转换并确定结果的类型。
§5.6 [expr.mul]
加减法类似:
对算术或枚举类型的操作数执行通常的算术转换。
§5.7 [expr.add]
浮点类型的常用算术转换在标准中如下所示:
许多期望算术或枚举类型的操作数的二元运算符会导致转换并以类似的方式产生结果类型。目的是产生一个通用类型,这也是结果的类型。这种模式称为通常的算术转换,定义如下:
[...]
——如果任一操作数的类型为
long double
,则另一个应转换为long double
。——否则,如果任一操作数为
double
,则另一个应转换为double
。——否则,如果任一操作数为
float
,则另一个应转换为float
。§5 [表达式]
这些浮点类型的实际形式/精度是实现定义的:
double
类型提供的精度至少与float
一样高,long double
类型提供的精度至少与double
一样高。float
类型的值集是double
类型的值集的子集;double
类型的值集是long double
类型的值集的子集。浮点类型的值表示是实现定义的。§3.9.1 [basic.fundamental]
【讨论】:
这个答案错过了问题的关键;这些计算在幕后执行的精度如何? 这是实现定义的。见§3.9.1 [basic.fundamental]。 我只能引用 std::limitsstd::numeric_limits
?【参考方案3】:
-
对于浮点乘法:FP 乘法器在内部使用操作数宽度的两倍来生成中间结果,该结果等于无限精度内的实际结果,然后将其四舍五入到目标精度。因此,您不必担心乘法。结果已正确四舍五入。
对于浮点加法,结果也被正确舍入,因为标准 FP 加法器使用额外足够的 3 个保护位来计算正确舍入的结果。
对于除法,余数和其他复杂的函数,如sin,log,exp等超越函数......它主要取决于架构和使用的库。如果您为除法或任何其他复杂函数寻求正确的舍入结果,我建议您使用 MPFR 库。
【讨论】:
有趣的是,您在 #1 中解决了我的主要问题,但是在 x86 和 long double(80 位扩展精度)的情况下,没有寄存器可以容纳两倍的宽度,对吗?我的意思是有四倍,但在 x86 中没有:) 谢谢,但我的意思是“乘法器在内部使用两倍于操作数的宽度”,这完全在乘法器本身内执行,无需您的干预。无论精度如何,您都会得到正确舍入的结果。例如,如果操作数是 80 位,即尾数为 64 位,则乘法器计算 124 位长的中间结果,然后再次将其舍入为 64 位长的结果,然后它将它与指数和符号一起保存到您的目标寄存器,构成一个 80 位长的结果。TLDR 你不应该担心 FP 加法和乘法。【参考方案4】:不是对您的问题的直接回答,但对于常量浮点值(例如您的问题中指定的值),产生最少精度损失的方法将使用每个值的有理表示整数分子除以整数分母,并在实际浮点除法之前执行尽可能多的整数乘法。
对于您问题中指定的浮点值:
int var1_num = 31;
int var1_den = 10;
int var2_num = 6789;
int var2_den = 1000;
int var3_num = 8745;
int var3_den = 100;
int var4_num = 234987;
int var4_den = 1000;
double result1 = (double)(var1_num*var2_num)/(var1_den*var2_den);
long double result2 = (long double)(var3_num*var4_num)/(var3_den*var4_den);
如果任何整数产品太大而无法放入int
,那么您可以使用更大的整数类型:
unsigned int
signed long
unsigned long
signed long long
unsigned long long
【讨论】:
以上是关于浮点算术运算的精度是多少?的主要内容,如果未能解决你的问题,请参考以下文章