给定三个“双精度”值“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`?

提问人:EFanZh 提问时间:3/15/2023 最后编辑:EFanZh 更新时间:3/16/2023 访问量:159

问:

我有三个值,和 ,我想计算 的结果,我可以写出以下函数:doublexyzx * 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.1y = 0.1z = 0.1
    • mul_div_1(x, y, z)给我,0.10000000000000002
    • mul_div_2(x, y, z)给我.0.1
  • 如果x = 23.0y = 13.0z = 23.0
    • mul_div_1(x, y, z)给我,13.0
    • mul_div_2(x, y, z)给我.12.999999999999998

有没有办法最大限度地减少计算时损失的精度?您可以假设 .x * y / zisfinite(x) && isfinite(y) && isfinite(z) && z != 0.0

精度损失不必绝对最小,有办法在以下两者之间选择最佳结果:

  • x * y / z
  • (x / z) * y
  • x * (y / z)

对我来说已经足够好了。

c 浮点 精度

评论

2赞 Tom Karzes 3/15/2023
这不会有太大区别。他们都会有错误。请记住,常量 like 没有精确的表示,因此在这种情况下,您从一开始就会遇到错误。与其试图获得确切的结果,不如在显示结果时对结果进行四舍五入,或定义某种容错值。0.1
0赞 EFanZh 3/15/2023
@TomKarzes 我知道 的不精确性,我对此感到满意。我只是想要某种策略来做某种适当的精度保护,而不需要实现绝对最小的精度损失。0.1
0赞 EFanZh 3/15/2023
@TomKarzes 此外,如果中间结果溢出,进动损失将产生差异。例如,如果我有 、 、 给我无穷大,然后给我 。我认为是无限的,并且足够不同。x = 1e200y = 1e200z = 1e200x * y / zx * (y / z)1e2001e200
0赞 n. m. could be an AI 3/15/2023
如果您的数据可以相差 200 个数量级,那么可能不是适合您的数据类型。double
0赞 user694733 3/15/2023
类似的问题,尽管不是重复的:在浮点插值上保持最大可能的精度

答:

2赞 ikegami 3/15/2023 #1

精度损失不必绝对最小

对于您给出的示例,精度的最大损失为 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

无论你采取哪种方法,你最多只能偏离一点。


对于加法和减法,您希望使用类似量级的数字。但是乘法和除法没有这样的限制。您只需要注意溢流和下溢。

评论

0赞 EFanZh 3/15/2023
事件 如果没有溢出,是否保证任何值最多偏离一位?
0赞 ikegami 3/15/2023
我没这么说。关键是你没有发现问题。另外,请参阅底部的部分。
0赞 ikegami 3/15/2023
但仔细想想,使用相同量级的数字对于加法和减法很重要,但我认为它与乘法和除法无关。
0赞 nielsen 3/15/2023
也许标准是避免获得非常接近或非常远离 0 的中间值。
1赞 Mark Ransom 3/16/2023
@EFanZh对于IEEE算术,可以保证单个操作的结果在一位以内。但当然,你正在做两个操作。
3赞 chtz 3/15/2023 #2

如果您只关心精度损失(不关心上溢/下溢),则可以使用融合乘法 () 以更高的精度计算乘积,并手动对其进行长除法: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;
}

如果您还必须处理上溢/下溢,您可以首先使用 ,对分数进行处理并加/减指数,然后使用 将数字重新组合为指数。frexpmul_divldexp

评论

1赞 njuffa 3/16/2023
在没有下溢/溢流的情况下,这会产生最大误差 ~= 0.5 ulp 的结果 如果除法很慢,可以改用: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; }
0赞 chux - Reinstate Monica 3/16/2023
@njuffa 很公平 - 评论已删除。
0赞 Brendan 3/16/2023 #3

有一些特殊情况:

  • 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 位有效,没有任何精度损失。xy

  • x或是 的倍数 ;首先进行相应的除法可以减少乘法中的精度损失。yz

除了这些情况(可能代替这些情况)之外,最简单的选择是使用更大的东西进行计算 - 要么解压缩成“符号、有效、指数”整数,然后自己用整数进行数学运算,或者 80 位扩展精度(也许如果你幸运的话)或 ,或成对的 .long double__float128double

0赞 chux - Reinstate Monica 3/16/2023 #4

如何将计算的精度损失降到最低,其中,并且都是类型?a * b / cabcdouble

无需求助于更高精度的数学运算

乘积和商,当与无限数学不完全相同时,每一步都会更高或更低 - 取决于舍入模式。*/

将普通回合与最接近,与偶数模式并列,结果将是最佳答案≤ 0.5 ULP

给定两个操作,则最大预期误差在 1.5 ULP 以内。@njuffa

当(中间)结果接近 2 的幂时,可能会出现一个极端情况,即 2 个步骤(然后或然后)将导致 2.0 ULP。*//*

使用 either 或 ,略微偏向于第一个。当 in 中的有效数字很少时,乘法是精确的,这与许多除法不同,因此代码只有 1 舍入的风险。mul_div_1()mul_div_2()a, b

具有更高精度的数学运算

当精度高于:long doubledouble

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

评论

1赞 njuffa 3/16/2023
回复“给定两次操作,最大预期误差在 1.0 ULP 以内”这太乐观了,它更像是复利。在不求助于具有误差边界的数学运算的情况下,它很容易通过运行大量随机测试向量来证明(当然不是证明),问题的最大误差为 1.5 ulpmul_div_1()mul_div_2()