如何计算两个浮点数的平均值?

How to compute mean of two floating-point numbers?

提问人:fdermishin 提问时间:9/25/2023 最后编辑:fdermishin 更新时间:9/26/2023 访问量:118

问:

在浮点运算中计算两个数字的平均值的最准确方法是什么?让我们考虑最常见的双精度 64 位数字。

  1. (a + b) / 2

  2. a / 2 + b / 2

  3. a + (b - a) / 2

这些计算平均值的方法可以给出不同的结果,如下面的 C++ 代码所示:

double a = 1.2;
double b = 3.6;
double mean1 = (a + b) / 2.0;
double mean2 = a / 2.0 + b / 2.0;
double mean3 = a + (b - a) / 2.0;
cout << fixed << setprecision(20);
cout << "mean1: " << mean1 << endl;
cout << "mean2: " << mean2 << endl;
cout << "mean3: " << mean3 << endl;

产生结果

mean1: 2.39999999999999991118
mean2: 2.39999999999999991118
mean3: 2.40000000000000035527

问题 两个浮点数的计算平均值的数学性质? 比较案例 1 和案例 2。简要的结论是,准确性是相同的,但情况 1 可能会溢出,而情况 2 可能会下溢。

mean1并且似乎提供了更准确的结果,因为它们比真实值 2.4 更接近。似乎总是情况不那么精确。直观地说,这是有道理的,因为 mean3 的计算需要 3 次运算,因此失去精度的机会更多,而 mean1 的计算只需要 2 次运算。mean2mean3mean3


更新:1.2 和 3.6 存储为

1.1999999999999999555910790149937383830547332763671875
3.600000000000000088817841970012523233890533447265625

所以他们真正的意思是

2.40000000000000002220446049250313080847263336181640625

但是,此值不能用双倍数表示。最接近的双精度值恰好是 ,其中 和 aremean1mean1mean3

2.399999999999999911182158029987476766109466552734375 (difference 1.11e-16)
2.4000000000000003552713678800500929355621337890625   (difference 3.33e-16)

我在 Fortran 代码 https://www.cs.umd.edu/~oleary/LBFGS/FORTRAN/linesearch.f 中偶然发现了第三种情况

stpf = stpc + (stpq - stpc)/2
stp = stx + p5*(sty - stx)

其中为 0.5p5

我不确定这种计算均值方式的动机是什么。可能是运行平均值的 n=2 的情况,如浮点值的数值稳定运行平均值https://diego.assencio.com/?index=c34d06f4f4de2375658ed41f70177d59 中所述,但它仅提高了大量元素的精度。

如果这是避免溢出的尝试,它似乎不起作用。这三种情况都可能产生溢流或下溢。

#include <iostream>
#include <iomanip>

using namespace std;

double mean1(double a, double b) {
    return (a + b) / 2.0;
}

double mean2(double a, double b) {
    return a / 2.0 + b / 2.0;
}

double mean3(double a, double b) {
    return a + (b - a) / 2.0;
}

void test(double a, double b) {
    cout << setprecision(20);
    cout << "a: " << a << ", b: " << b << endl;
    cout << "mean1: " << mean1(a, b) << endl;
    cout << "mean2: " << mean2(a, b) << endl;
    cout << "mean3: " << mean3(a, b) << endl;
    cout << endl;
}

int main() {
    test(1e+308, 1e+308); // case 1 overflow
    test(4e-324, 4e-324); // case 2 underflow
    test(-1e+308, 1e+308); // case 3 overflow
}
a: 1.000000000000000011e+308, b: 1.000000000000000011e+308
mean1: inf
mean2: 1.000000000000000011e+308
mean3: 1.000000000000000011e+308

a: 4.9406564584124654418e-324, b: 4.9406564584124654418e-324
mean1: 4.9406564584124654418e-324
mean2: 0
mean3: 4.9406564584124654418e-324

a: -1.000000000000000011e+308, b: 1.000000000000000011e+308
mean1: 0
mean2: 0
mean3: inf

此外,第三种情况的性能稍差一些(但是应谨慎考虑结果,因为它们可能因上下文而异,但通常具有较少指令的代码运行速度更快)

static void case_1(benchmark::State& state) {
  volatile double a = 1.2;
  volatile double b = 3.6;
  for (auto _ : state) {
    benchmark::DoNotOptimize((a + b) / 2.0);
  }
}
BENCHMARK(case_1);

static void case_2(benchmark::State& state) {
  volatile double a = 1.2;
  volatile double b = 3.6;
  for (auto _ : state) {
    benchmark::DoNotOptimize(a / 2.0 + b / 2.0);
  }
}
BENCHMARK(case_2);

static void case_3(benchmark::State& state) {
  volatile double a = 1.2;
  volatile double b = 3.6;
  for (auto _ : state) {
    benchmark::DoNotOptimize(a + (b - a) / 2.0);
  }
}
BENCHMARK(case_3);

https://quick-bench.com/q/V_STrIZ5CmIL0Q3KpbLX6G9-G90

可能我错过了这个选择背后的一些逻辑。上面的代码可能是为了在不支持 IEEE 754 的不同硬件上运行?那么,有什么理由计算均值而不是?或者它只是轻微的不准确,应该使用简单的方法来计算平均值?a + (b - a) / 2(a + b) / 2

与浮点 语言无关的 IEEE-754

评论

0赞 trincot 9/25/2023
如您所知,浮点的精度有限,因此从一开始,1.2 在存储在浮点表示中时并不完全是这个数字。如果您必须担心在顶部列出的差异,那么这表明您一开始使用了错误的数据类型。
1赞 n. m. could be an AI 9/25/2023
没有完全等于 或 或 的值。您正在计算 1.199999999999999999555910790149937383830547332763671875 和 3.6000000000000000088817841970012523233890533447265625 的平均值。double1.23.62.4
0赞 fdermishin 9/25/2023
我已经更新了问题并澄清了这种情况。 尽可能准确地计算结果,使结果可以表示为 double ,但会损失一些精度。(a + b) / 2a + (b - a) / 2
1赞 vinc17 9/26/2023
这回答了你的问题吗?浮点数的最佳中点公式是什么?
2赞 chtz 9/26/2023
相关 CppCon 讲座:std::midpoint?这有多难?-- 浮点型从48:40左右开始处理

答:

4赞 Matt Timmermans 9/25/2023 #1

为了准确性,或者应该是首选,因为除以 2 在浮点中是无损的,除非它“下溢”。只是减去指数,所以,实际上,误差只在加法中引入。(a + b) / 2a / 2 + b / 2/ 2

如果你愿意,你可以代替 - 这没有区别。* 0.5/ 2.0

然而,该表格保证:a + (b - a) / 2

  • 如果 和 都是非负有限值,则结果也是如此。该表格不提供此保证;和ab(a + b)/2
  • 如果 和 都是非负有限值,则 和 ,则结果始终为 和 。其他两种形式都不能提供这种保证。这可能很重要,例如,如果您正在执行二进制搜索。abb >= a>= a<= b

评论

0赞 fdermishin 9/25/2023
谢谢,如果保证值为非负数,这是有道理的
0赞 aka.nice 9/26/2023
这个答案明确地将应用程序限制为有限值,但请注意,当 a 是无穷大时,它提供 nan 而不是 inf 的缺陷(这是不正确的,除非 b 也是 -inf,我们用非否定断言来限制)。a + (b - a) / 2
0赞 chux - Reinstate Monica 9/27/2023
除非你指的是溢出,否则不同意“如果 a 和 b 都是非负有限值,那么结果也是如此。表格不提供此保证“ --> 请提供一个反例,当 , 不会导致 。(a + b)/2a >= 0a >= 0(a + b)/2>= 0
0赞 fdermishin 9/27/2023
@chux-恢复莫妮卡 非负有限 a 和 b 保证是非负的,但不是说它是有限的。因此,语句“是非负有限值”可以是假的。(a + b)/2(a + b)/2
2赞 chux - Reinstate Monica 9/26/2023 #2
  • 避免溢出:两者都有无穷大的结果,完全失去所有的进动。a + bb - a

  • 请注意,浮点编码可能支持也可能不支持次正态。因此,接近这些值的数学很难实现可移植性。

  • a/2只有在以下情况下(在正常范围的末尾)并且具有选择值时才可能会失去精度。对于大多数值,不会产生舍入。在精度损失的情况下,结果仍然是有限的,这与溢出不同。这通常是更容易忍受的。|a| < DBL_MIN*2aa/2


鉴于这些问题,当任一值的大小不小(不接近)时,是更好的选择。a / 2 + b / 2DBL_MIN


在浮点运算中计算两个数字的平均值的最准确方法是什么?

在不求助于更广泛的数学运算的情况下,避免所有溢出和大多数下溢问题的一种方法是使计算接近 1.0。这至少可以用作比较其他代码的参考函数。

double avg(double a, double b) {
  // Extract exponent.
  int expoa, expob;
  a = frexp(a, &expoa);  // |a| is now in the [0.5...1.0) range, or 0.
  b = frexp(b, &expob);

  // Scale the exponent.
  int expo = max(expoa, expob);
  a = ldexp(a, expoa - expo);
  b = ldexp(b, expob - expo);
  double avg = (a + b)/2;
  return ldexp(avg, expo);
}

我断言,当 1 为零、非零和 .otherother/2 --> 0.0


某些语言允许使用更广泛的类型进行中间计算,从而使选择变得毫无意义。所有 3 个答案都相同。

x * 0.5并且可能与前者不同,因为前者可能会产生浮点类型提升。(例如 到操作。
很少这样做。
x / 2.0x / 2float xdoublex / 2

评论

0赞 aka.nice 9/26/2023
缩放可能会起作用,除非 ldexp 实现有问题......为什么会有问题?好吧,它在 Microsoft 和 Apple MacOS 库上都有错误......还记得 stackoverflow.com/questions/32150888/... 例如......
0赞 aka.nice 9/26/2023
Ah, I found the Apple one, follow github.com/OpenSmalltalk/opensmalltalk-vm/issues/383
0赞 chux - Reinstate Monica 9/26/2023
@aka.nice That bug does not come up in this answer's as the scaling never is more than the original. Perhaps with when are near and differ in sign - do you have a sample in mind?ldexp(a, expoa - expo); b = ldexp(b, expob - expo);ldexp(avg, expo)a, bDBL_MIINavg(a,b)
0赞 aka.nice 9/27/2023
yes when a and b are near DBL_MIN, on windows. take and ; should round to nearest even, that is to upper . I guess the final will answer the wrong result with MSVC, at least up to 2017, did not try more recent ones.a=ldexp(5.0,-1074);b=ldexp(10.0,-1074);(a+b)/2ldexp(8.0,-1074)return ldexp(avg, expo);
0赞 aka.nice 9/27/2023
double a = ldexp(5.0, -1074); double b = ldexp(10.0, -1074); printf("(a+b)/2=%a avg(a,b)=%a \n", (a+b)/2, avg(a,b)); does indeed print two different results compiled with MSVC 2017 .(a+b)/2=0x0.0000000000008p-1022 avg(a,b)=0x0.0000000000007p-1022