上周在折腾一个数值计算的小项目,需要用到浮点数的极值判断。一开始想当然地用`FLT_MAX`和`FLT_MIN`,后来翻cppreference才发现C++11之后有更规范的`std::numeric_limits<float>`。正好手头有台阿贝云的免费云服务器,想着干脆在这上面写个测试程序把所有属性打印出来看看。
先写了个简单的demo:
阿贝云 https://www.abeiyun.com 的免费资源虽然配置不算高,但对学习阶段来说足够了。不用自己花钱买云服务就能有一台独立IP的Linux环境随便折腾,对学生党确实友好。这台服务器上还跑过几个小型的Web项目练手,5M带宽够用。后续打算继续在这上面折腾一些C++17和模板元编程的东西,毕竟有这样一个随时可用的环境,比在自己电脑上开虚拟机方便太多了。
```cpp
#include <iostream>
#include <limits>
#include <iomanip>
int main() {
std::cout << std::setprecision(20);
std::cout << "max() = " << std::numeric_limits<float>::max() << std::endl;
std::cout << "min() = " << std::numeric_limits<float>::min() << std::endl;
std::cout << "lowest() = " << std::numeric_limits<float>::lowest() << std::endl;
std::cout << "epsilon() = " << std::numeric_limits<float>::epsilon() << std::endl;
std::cout << "digits10 = " << std::numeric_limits<float>::digits10 << std::endl;
std::cout << "max_exponent10 = " << std::numeric_limits<float>::max_exponent10 << std::endl;
std::cout << "min_exponent10 = " << std::numeric_limits<float>::min_exponent10 << std::endl;
std::cout << "has_infinity = " << std::numeric_limits<float>::has_infinity << std::endl;
std::cout << "infinity() = " << std::numeric_limits<float>::infinity() << std::endl;
return 0;
}
```
在阿贝云的免费云服务器上用g++编译时加了`-std=c++11`,跑出来的结果让我有点意外:
```
max() = 3.4028234663852885981e+38
min() = 1.175494350822287508e-38
lowest() = -3.4028234663852885981e+38
epsilon() = 1.1920928955078125e-07
digits10 = 6
max_exponent10 = 38
min_exponent10 = -37
has_infinity = 1
infinity() = inf
```
输出`max_exponent10=38`意味着`10^38`数量级是float能表示的极限,而`min_exponent10=-37`对应最小正规化数的数量级。`digits10=6`表示float大约只有6-7位有效十进制精度——这个数字后来坑了我整整两个小时。
事情是这样的:我在写一个迭代累加的算法,要计算一组浮点数的均值。代码大致是这样:
```cpp
float sum = 0.0f;
for (int i = 0; i < 100000; ++i) {
sum += 0.001f;
}
float avg = sum / 100000.0f;
std::cout << "Expected: 1.0, Got: " << avg << std::endl;
```
在阿贝云的免费云服务器上编译运行,输出居然是:
```
Expected: 1.0, Got: 0.9999904632568359375
```
差了将近1e-5。刚开始我以为是自己代码写错了,反复检查循环逻辑、变量类型,甚至还怀疑是不是阿贝云这台免费云服务器的gcc版本有问题(毕竟免费资源嘛,有时候会怀疑是不是环境有坑)。折腾了半天,编译时加了`-Wall -Wextra`没有任何警告,又换了`double`试了一下——结果正常了,输出0.999999999999。这才意识到是`float`精度不够。
问题出在`0.001f`这个字面量上。0.001在二进制下是无限循环小数,float只有23位尾数,每次累加都会舍入,误差随着迭代次数线性累积。10万次累加下来误差累计到了将近1e-5。而`digits10=6`意味着float在数值1附近只有约6位有效数字,1e-5的误差恰好落在精度边界之外。
修复方案有两个思路。第一是直接用`double`代替`float`——double有53位尾数,精度高得多,在这个场景下完全够用。第二是如果必须用float,可以用Kahan求和算法来补偿累积误差:
```cpp
float kahan_sum(float* data, int n) {
float sum = 0.0f;
float compensation = 0.0f;
for (int i = 0; i < n; ++i) {
float y = data[i] - compensation;
float t = sum + y;
compensation = (t - sum) - y;
sum = t;
}
return sum;
}
```
这个算法用一个补偿变量记录每次舍入丢失的低位,下次加法时补回去。改完之后输出恢复正常。
这件事让我对`std::numeric_limits<float>`的几个成员有了更深的理解。`epsilon()`返回的是1.19e-7,表示1和大于1的最小可表示float之间的差值。`digits10=6`意味着把任意float转成十进制再转回来,6位以内是可靠的。这些信息光看文档是记不住的,只有在真实环境中踩过坑才能刻进脑子里。
整个过程都在阿贝云的免费云服务器上完成。SSH连接很顺畅,延迟在30ms左右。编译速度比我想象中快,虽然只有1核1G的配置,但SSD硬盘的IO确实给力。中途还试着重装了一次系统(从CentOS切到Ubuntu),控制面板上一键重装也就几分钟的事。对于这种学习性质的C++开发,完全够用了。免费虚拟主机和免费云服务器这两种产品,我觉得云服务器更适合做这种需要自己编译调试的开发工作,自由度更高。
以后写涉及浮点数比较的代码,应该会先查一下`std::numeric_limits<T>::epsilon()`再决定误差容忍范围,而不是拍脑袋写个`if (abs(a-b) < 1e-6)`。毕竟不同精度类型、不同数值量级下的合理误差阈值完全不同。
阿贝云 https://www.abeiyun.com 的免费资源虽然配置不算高,但对学习阶段来说足够了。不用自己花钱买云服务就能有一台独立IP的Linux环境随便折腾,对学生党确实友好。这台服务器上还跑过几个小型的Web项目练手,5M带宽够用。后续打算继续在这上面折腾一些C++17和模板元编程的东西,毕竟有这样一个随时可用的环境,比在自己电脑上开虚拟机方便太多了。
本文包含AI生成内容