提问人:fdermishin 提问时间:9/25/2023 最后编辑:fdermishin 更新时间:9/26/2023 访问量:118
如何计算两个浮点数的平均值?
How to compute mean of two floating-point numbers?
问:
在浮点运算中计算两个数字的平均值的最准确方法是什么?让我们考虑最常见的双精度 64 位数字。
(a + b) / 2
a / 2 + b / 2
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 次运算。mean2
mean3
mean3
更新:1.2 和 3.6 存储为
1.1999999999999999555910790149937383830547332763671875
3.600000000000000088817841970012523233890533447265625
所以他们真正的意思是
2.40000000000000002220446049250313080847263336181640625
但是,此值不能用双倍数表示。最接近的双精度值恰好是 ,其中 和 aremean1
mean1
mean3
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
答:
为了准确性,或者应该是首选,因为除以 2 在浮点中是无损的,除非它“下溢”。只是减去指数,所以,实际上,误差只在加法中引入。(a + b) / 2
a / 2 + b / 2
/ 2
如果你愿意,你可以代替 - 这没有区别。* 0.5
/ 2.0
然而,该表格保证:a + (b - a) / 2
- 如果 和 都是非负有限值,则结果也是如此。该表格不提供此保证;和
a
b
(a + b)/2
- 如果 和 都是非负有限值,则 和 ,则结果始终为 和 。其他两种形式都不能提供这种保证。这可能很重要,例如,如果您正在执行二进制搜索。
a
b
b >= a
>= a
<= b
评论
a + (b - a) / 2
(a + b)/2
a >= 0
a >= 0
(a + b)/2
>= 0
(a + b)/2
(a + b)/2
避免溢出:两者都有无穷大的结果,完全失去所有的进动。
a + b
b - a
请注意,浮点编码可能支持也可能不支持次正态。因此,接近这些值的数学很难实现可移植性。
a/2
只有在以下情况下(在正常范围的末尾)并且具有选择值时才可能会失去精度。对于大多数值,不会产生舍入。在精度损失的情况下,结果仍然是有限的,这与溢出不同。这通常是更容易忍受的。|a| < DBL_MIN*2
a
a/2
鉴于这些问题,当任一值的大小不小(不接近)时,是更好的选择。a / 2 + b / 2
DBL_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 为零、非零和 .other
other/2 --> 0.0
等
某些语言允许使用更广泛的类型进行中间计算,从而使选择变得毫无意义。所有 3 个答案都相同。
x * 0.5
并且可能与前者不同,因为前者可能会产生浮点类型提升。(例如 到操作。
很少这样做。x / 2.0
x / 2
float x
double
x / 2
评论
ldexp(a, expoa - expo); b = ldexp(b, expob - expo);
ldexp(avg, expo)
a, b
DBL_MIIN
avg(a,b)
a=ldexp(5.0,-1074);
b=ldexp(10.0,-1074);
(a+b)/2
ldexp(8.0,-1074)
return ldexp(avg, expo);
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
评论
double
1.2
3.6
2.4
(a + b) / 2
a + (b - a) / 2