提问人:EFanZh 提问时间:3/15/2023 最后编辑:EFanZh 更新时间:3/16/2023 访问量:159
给定三个“双精度”值“x”、“y”和“z”,如何最小化计算“x * y / z”的精度损失?
Given three `double` values `x`, `y` and `z`, how to minimize the precision loss of calculating `x * y / z`?
问:
我有三个值,和 ,我想计算 的结果,我可以写出以下函数:double
x
y
z
x * y / z
double mul_div_1(double x, double y, double z) {
return x * y / z;
}
double mul_div_2(double x, double y, double z) {
return x * (y / z);
}
问题在于,这两个函数在任何时候都不能提供最准确的结果。例如:
- 如果
x = 0.1
y = 0.1
z = 0.1
mul_div_1(x, y, z)
给我,0.10000000000000002
mul_div_2(x, y, z)
给我.0.1
- 如果
x = 23.0
y = 13.0
z = 23.0
mul_div_1(x, y, z)
给我,13.0
mul_div_2(x, y, z)
给我.12.999999999999998
有没有办法最大限度地减少计算时损失的精度?您可以假设 .x * y / z
isfinite(x) && isfinite(y) && isfinite(z) && z != 0.0
精度损失不必绝对最小,有办法在以下两者之间选择最佳结果:
x * y / z
(x / z) * y
x * (y / z)
对我来说已经足够好了。
答:
精度损失不必绝对最小
对于您给出的示例,精度的最大损失为 1 位。
x=0.1 y=0.1 z=0.1
十六进制/二进制/十进制 | |
---|---|
0.1 | 0X1.9999999999999AP-4 0.00011001100110011001100110011001100110011001100110011010 0.1000000000000000055511151231257827021181583404541015625 |
(x*y)/z | 0X1.9999999999999BP-4 0.0001100110011001100110011001100110011001100110011001100110110.10000000000000001942890293094023945741355419158935546875 |
(x/z)*y | 0X1.9999999999999AP-4 0.00011001100110011001100110011001100110011001100110011010 0.1000000000000000055511151231257827021181583404541015625 |
(y/z)*x | 0X1.9999999999999AP-4 0.00011001100110011001100110011001100110011001100110011010 0.1000000000000000055511151231257827021181583404541015625 |
无论你采取哪种方法,你最多只能偏离一点。
x=23 y=13 z=23
十六进制/二进制/十进制 | |
---|---|
13 | 0X1。A000000000000P+3 1101.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 |
(x*y)/z | 0X1。A000000000000P+3 1101.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 |
(x/z)*y | 0X1。A000000000000P+3 1101.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 |
(y/z)*x | 0X1.9FFFFFFFFFFFFP+3 1100.11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 |
无论你采取哪种方法,你最多只能偏离一点。
对于加法和减法,您希望使用类似量级的数字。但是乘法和除法没有这样的限制。您只需要注意溢流和下溢。
评论
如果您只关心精度损失(不关心上溢/下溢),则可以使用融合乘法 () 以更高的精度计算乘积,并手动对其进行长除法:fma
#include <math.h>
double mul_div(double x, double y, double z)
{
double xy = x*y; // higher bits of product
double xy_ = fma(x, y, -xy); // remaining bits of product
double q = xy / z; // higher bits of quotient
double r= xy_ + fma(-q, z, xy); // remainder of division, added to lower part of product
q += r / z; // add quotient of remainder
return q;
}
如果您还必须处理上溢/下溢,您可以首先使用 ,对分数进行处理并加/减指数,然后使用 将数字重新组合为指数。frexp
mul_div
ldexp
评论
double mul_div (double x, double y, double z) { double ph, pl, qh, ql, d, r; ph = x * y; pl = fma (x, y, -ph); r = 1 / z; qh = ph * r; d = fma (z, -qh, ph); qh = fma (r, d, qh); ql = fma (z, -qh, ph); ql = ql + pl; d = ql * r; ql = fma (z, -d, ql); ql = fma (r, ql, d); return qh + ql; }
有一些特殊情况:
fabs(x) == fabs(z)
,确定符号并返回完成或 。y
-y
fabs(y) == fabs(z)
,确定符号并返回完成或 。x
-x
有效位和的已用位之和(或总位减去尾随的零位)小于或等于有效位的位数;因此,乘法不会导致精度损失,应该先做(因为先做除法可能会导致乘法中可避免的精度损失)。例如,1.5 是 2 位(二进制为 1.1),1.0625 是 4 位(二进制为 1.001),2+4 = 6 位,这将适合 53 位有效,没有任何精度损失。
x
y
x
或是 的倍数 ;首先进行相应的除法可以减少乘法中的精度损失。y
z
除了这些情况(可能代替这些情况)之外,最简单的选择是使用更大的东西进行计算 - 要么解压缩成“符号、有效、指数”整数,然后自己用整数进行数学运算,或者 80 位扩展精度(也许如果你幸运的话)或 ,或成对的 .long double
__float128
double
如何将计算的精度损失降到最低,其中,并且都是类型?
a * b / c
a
b
c
double
无需求助于更高精度的数学运算
乘积和商,当与无限数学不完全相同时,每一步都会更高或更低 - 取决于舍入模式。*
/
将普通回合与最接近,与偶数模式并列,结果将是最佳答案≤ 0.5 ULP。
给定两个操作,则最大预期误差在 1.5 ULP 以内。@njuffa。
当(中间)结果接近 2 的幂时,可能会出现一个极端情况,即 2 个步骤(然后或然后)将导致 2.0 ULP。*
/
/
*
使用 either 或 ,略微偏向于第一个。当 in 中的有效数字很少时,乘法是精确的,这与许多除法不同,因此代码只有 1 舍入的风险。mul_div_1()
mul_div_2()
a, b
具有更高精度的数学运算
当精度高于:long double
double
double mul_div_1(double x, double y, double z) {
return (long double) x * y / z;
}
测试 2 条路径中的一条是否没有错误
虽然不是 100% 保证测试来检测除法是否准确,但它很接近。fmod(y,z) == 0
#include <math.h>
double mul_div_3(double x, double y, double z) {
if (fmod(y,z) == 0) {
puts("x * (y / z)");
return x * (y / z);
}
puts("x * y / z");
return x * y / z;
}
#include <float.h>
#include <stdio.h>
int main() {
printf("%.*g\n", DBL_DECIMAL_DIG, mul_div_3(0.1, 0.1, 0.1));
printf("%.*g\n", DBL_DECIMAL_DIG, mul_div_3(23, 13, 23));
}
输出
x * (y / z)
0.10000000000000001
x * y / z
13
评论
mul_div_1()
mul_div_2()
评论
0.1
0.1
x = 1e200
y = 1e200
z = 1e200
x * y / z
x * (y / z)
1e200
1e200
double