深入了解浮点数在计算机中的存储方式和运算
浮点数我们都很熟悉,但它在计算机中是怎么存储和运算的?它和int类型数据运算,哪个速度快?带着这些疑问,博主又重新学习了浮点数相关的知识(以前学过,但有些知识点有点模糊了)
先给结论
浮点数不是像int那样“直接存一个值”,而是按科学计数法的二进制版本来存。
最常见的是 IEEE 754 标准格式,本质上拆成三部分:
- 符号位
- 指数位
- 尾数位
它和int比,谁更快没有绝对答案。通常可以这样记:
- 纯整数加减、比较、位运算,
int更直接 - 现代 CPU 上,浮点加减乘也很快,不一定明显慢很多
- 混合
int和浮点运算会有类型转换,通常更慢一点 - 真正影响性能的,很多时候不只是算术本身,还有缓存、内存带宽、是否有 FPU、是否能 SIMD 并行
一、浮点数是怎么存的
最常见的浮点类型:
float:通常 32 位double:通常 64 位long double:实现相关,不同平台可能不同
以最常见的 IEEE 754 为例:
1. float 的常见布局
- 1 位符号位
- 8 位指数位
- 23 位尾数位
也就是:32=1+8+23
2. double 的常见布局
- 1 位符号位
- 11 位指数位
- 52 位尾数位
也就是:64=1+11+52
二、浮点数的值怎么还原
以正规浮点数为例,值大致是:
(−1)sign×1.fraction×2exponent−bias
这里:
sign表示正负fraction是尾数部分exponent是指数bias是指数偏移量
常见偏移量:
float的 bias 是127double的 bias 是1023
所以浮点数其实就是:
符号 × 有效数字 × 2 的指数次幂
这就是“二进制科学计数法”。
三、为什么浮点数会有精度误差
因为浮点数是二进制小数,不是十进制小数。
像这些数可以精确表示:
0.5,因为它是 2−12−10.25,因为它是 2−22−20.125,因为它是 2−32−3
但像0.1这种十进制小数,在二进制里通常是无限循环的,没法精确存,只能存一个最接近它的近似值。
例如:
#include <iostream> #include <iomanip> int main() { double x = 0.1; std::cout << std::setprecision(20) << x << '\n'; }常见输出类似:0.10000000000000000555
这不是显示错了,而是内存里存的本来就是近似值。
四、浮点数的特殊值
浮点数除了普通数,还能表示这些特殊情况:
+0和-0+∞和-∞NaN,也就是 Not a Number
例如:
#include <iostream> int main() { double inf = 1.0 / 0.0; double nan = 0.0 / 0.0; std::cout << inf << '\n'; std::cout << nan << '\n'; }五、浮点数是怎么运算的
现代 CPU 一般有专门的浮点运算单元,有时还能用 SIMD 指令一次处理多个浮点数。
不同运算的底层思路不一样。
1. 浮点加法 / 减法
大致步骤:
- 比较两个数的指数
- 把指数小的那个尾数右移,对齐指数
- 尾数相加或相减
- 结果规格化
- 按舍入规则取最近值
所以浮点加减比整数加减复杂得多。
2. 浮点乘法
大致步骤:
- 符号位异或得到结果符号
- 指数相加再减去偏移量
- 尾数相乘
- 结果规格化和舍入
3. 浮点除法
大致步骤:
- 符号位处理
- 指数相减
- 尾数相除
- 结果规格化和舍入
六、浮点运算为什么比 int 更复杂
因为它不仅仅是在“算数值”,还要处理这些问题:
- 指数对齐
- 规格化
- 舍入
- 溢出和下溢
NaN和无穷大- 非正规数
而int通常只是固定宽度的二进制整数计算,规则更直接。
七、int 是怎么存的
现代计算机里,int通常用补码表示。
比如常见的 32 位int:
- 正数直接按二进制存
- 负数按补码存
它的特点是:
- 表示的是精确整数
- 没有浮点舍入问题
- 加减乘逻辑更简单
- 适合计数、下标、位运算
八、float / double 和 int 混合运算时发生什么
如果表达式里同时有int和浮点数,通常会先把int转成浮点数,再做浮点运算。
例如:
int a = 3; double b = 0.5; double c = a + b;底层更接近:
- 先把
a变成3.0 - 再做
3.0 + 0.5
所以混合运算通常会有:
- 一次类型转换
- 浮点规则参与
- 可能的额外开销
九、哪个更快
这是最关心的部分。
结论是:看场景。
1. 纯整数加减乘
通常int更直接,很多时候也更快或至少不慢。
尤其是:
- 比较
- 位运算
- 取模
- 下标和地址计算
这些是整数的强项。
2. 浮点加减乘
在现代桌面 CPU 上,float和double的加减乘也很快,很多时候吞吐量并不差,未必比int慢很多。
所以不要简单理解成:
“浮点一定特别慢”
这在现代 CPU 上通常不成立。
3. 除法
无论整数除法还是浮点除法,都明显比加减乘慢。
但一般来说:
- 浮点除法比较贵
- 整数除法也不便宜
所以如果代码里大量做除法,性能瓶颈常常在这里。
4. 混合类型运算
int + float、int + double这种混合运算,通常比纯整数或纯浮点更麻烦,因为要做类型提升。
5. 批量数组计算
这里不仅是“算一次快不快”的问题,还和数据大小有关。
常见大小:
int常见是 4 字节float是 4 字节double是 8 字节
所以:
float和int在内存占用上常常差不多double更大,更吃缓存和带宽- 大数据遍历时,
double可能因为内存压力整体更慢
十、不同平台差异很大
这一点非常重要。
1. 桌面 CPU / 服务器 CPU
现代 x86、ARM 大多有很强的 FPU,浮点性能通常不错。
这类机器上,浮点加减乘不一定明显慢。
2. 单片机 / 没有硬件 FPU 的平台
这类平台浮点可能要靠软件模拟,性能会非常差。
在这种环境下,浮点往往明显慢于整数。
所以如果题目是面试题,比较标准的回答应该是:
在现代通用 CPU 上,浮点加减乘通常也很快;但在没有硬件浮点单元的嵌入式平台上,浮点通常会比整数慢很多。
十一、一个精度例子
#include <iostream> #include <iomanip> int main() { double a = 0.1; double b = 0.2; double c = a + b; std::cout << std::setprecision(20) << c << '\n'; std::cout << (c == 0.3) << '\n'; }很多环境下输出类似:
0.30000000000000004441
0
说明:
- 浮点数存的是近似值
- 运算结果也可能有误差
- 不能随便用
==比较浮点数
十二、一个混合运算例子
#include <iostream> int main() { int a = 5; float b = 2.5f; auto c = a + b; std::cout << c << '\n'; }这里会先把a转成5.0f,再做浮点加法。
十三、怎么选类型
可以直接按语义来选。
优先用 int 的场景:
- 计数
- 下标
- 个数
- 离散状态
- 位操作
- 需要精确整数结果
优先用 float / double 的场景:
- 小数
- 坐标
- 概率
- 比例
- 物理量
- 科学计算
通常:
- 对精度要求一般,用
float - 对精度要求更高,大多数通用数值计算用
double
十四、面试里怎么答
可以直接这样说:
浮点数通常按 IEEE 754 标准存储,使用符号位、指数位和尾数位表示,本质上是二进制科学计数法。它能表示很大范围的实数,但很多十进制小数无法精确表示,所以会有精度误差。浮点运算由硬件浮点单元完成,内部会做指数对齐、尾数运算、规格化和舍入,因此比整数运算规则更复杂。至于速度,现代 CPU 上浮点加减乘也很快,不一定比 int 慢很多;但整数运算通常更直接,混合运算有转换成本,而在没有 FPU 的平台上浮点会明显更慢。
一句话总结
浮点数是用“符号 + 指数 + 尾数”按二进制科学计数法存的,能表示小数和很大范围,但很多值只是近似值;速度上不能绝对说谁更快,但通常整数更直接,浮点更复杂,而现代 CPU 上浮点加减乘也已经非常快。
1. 为什么会出现 0.1 + 0.2 != 0.3
本质原因只有一句话:
十进制里的 0.1 和 0.2,通常不能被二进制浮点数精确表示,所以内存里存的是两个“最接近它们的近似值”,相加后得到的也是近似值,不一定恰好等于 0.3 的近似值。
可以把浮点数理解成二进制科学计数法:
value=(−1)s×1.f×2e
其中:
- s 是符号位
- f 是尾数
- e 是指数
问题在于,像 0.5、0.25、0.125 这种数能精确写成,所以二进制里能精确表示。
但 0.1 在二进制里是无限循环小数,类似十进制里的 1/3=0.3333…1/3=0.3333…。
所以计算机里实际保存的不是“精确的 0.1”,而是:
- 一个最接近 0.1 的二进制值
- 一个最接近 0.2 的二进制值
- 它们相加以后,得到一个最接近 0.3 的结果,但未必正好等于程序里写的 0.3 的内部表示
看一个例子:
#include <iostream> #include <iomanip> int main() { double a = 0.1; double b = 0.2; double c = a + b; std::cout << std::setprecision(20) << a << '\n'; std::cout << std::setprecision(20) << b << '\n'; std::cout << std::setprecision(20) << c << '\n'; std::cout << (c == 0.3) << '\n'; }常见输出类似:
0.10000000000000000555
0.20000000000000001110
0.30000000000000004441
0
所以浮点数比较时,通常不要直接用 ==,而是看误差是否足够小:
#include <cmath> #include <iostream> int main() { double a = 0.1 + 0.2; double b = 0.3; double eps = 1e-12; if (std::fabs(a - b) < eps) { std::cout << "almost equal\n"; } }一句话记忆:
浮点数能表示“很大范围的小数”,但很多十进制小数只是近似存储,不是精确存储。
2. float、double、int 的存储结构和性能对比
先看最核心的结构区别。
| 类型 | 常见大小 | 存储方式 | 能否精确表示整数 | 能否表示小数 | 典型用途 |
|---|---|---|---|---|---|
| int | 4 字节 | 补码整数 | 可以,在范围内精确 | 不行 | 计数、下标、离散值 |
| float | 4 字节 | IEEE 754 单精度浮点 | 只能精确表示一部分整数 | 可以,但有误差 | 图形、实时计算、对内存敏感的数值 |
| double | 8 字节 | IEEE 754 双精度浮点 | 只能精确表示一部分整数 | 可以,精度高于 float | 通用数值计算、工程计算 |
int 是怎么存的
int 通常直接按二进制整数存,现代机器大多用补码。
优点:
- 表示整数是精确的
- 加减比较直接
- 位运算特别高效
- 没有浮点舍入问题
缺点:
- 不能表示小数
- 表示范围有限
float 和 double 是怎么存的
最常见的是 IEEE 754。
float 一般是 32 位:
- 1 位符号位
- 8 位指数位
- 23 位尾数位
double 一般是 64 位:
- 1 位符号位
- 11 位指数位
- 52 位尾数位
所以:
- float 占用更小
- double 精度更高
- 两者都属于近似表示
可以粗略理解成:
- float 是“范围够大,但精度一般”
- double 是“范围大,精度更高”
- int 是“不能表示小数,但整数精确”
谁更快
不能简单说绝对谁快,但可以按场景记:
1. 纯整数运算
通常 int 更直接,尤其是:
- 加减
- 比较
- 位运算
- 下标计算
- 取模
这类操作一般是整数强项。
2. 纯浮点加减乘
现代 CPU 上,float 和 double 的加减乘都已经很快了,不一定比 int 慢很多。
也就是说:
“浮点一定很慢”这个说法,在现代桌面 CPU 上通常不准确。
3. 除法
无论整数除法还是浮点除法,都比加减乘慢不少。
但一般来说:
- 浮点除法比较贵
- 整数除法也不便宜
所以不要把性能瓶颈简单归结成“是不是浮点”。
4. 混合运算
比如:
int a = 3; double b = 0.5; auto c = a + b;这里通常会先把 int 转成 double,再做浮点运算。
所以混合类型运算往往比纯 int 或纯 double 更绕,也常常更慢一点。
5. 大规模数组计算
这里内存和缓存很关键。
- int 常见是 4 字节
- float 是 4 字节
- double 是 8 字节
因此:
- 同样缓存大小下,能装更多 int 和 float
- double 更吃内存带宽
- 批量遍历时,double 可能因为数据更大而整体更慢
所以很多时候,真正拖慢程序的不是“算术单次速度”,而是“数据搬运成本”。
一个很实用的性能判断表
| 场景 | 通常更占优的类型 |
|---|---|
| 计数、下标、位运算 | int |
| 一般小数计算 | double |
| 对内存更敏感的大量浮点数组 | float 可能更有优势 |
| 高精度通用数值计算 | double |
| 需要精确整数语义 | int |
| 混合 int 和浮点频繁参与计算 | 尽量统一类型,避免来回转换 |
怎么选最合理
- 数据本质是整数,就用 int
- 数据本质是实数,就用 float 或 double
- 默认数值计算,double 往往比 float 更稳妥
- 如果特别在意性能,不要靠猜,直接做 benchmark
- 如果特别在意内存和带宽,float 和 int 往往比 double 更省
一句话总结
0.1 + 0.2 不等于 0.3,是因为浮点数按二进制科学计数法近似存储,很多十进制小数无法精确表示。
性能上,int 通常更直接,float 和 double 在现代 CPU 上也很快,但 double 更占内存;真正谁更快,要看运算类型、数据规模、是否混合类型以及缓存和带宽。
1. IEEE 754 单精度和双精度逐位拆解
先记总公式。对“正规浮点数”,它的值一般写成:
(−1)s×1.f×2e−bias
这里:
- ss 是符号位
- ff 是尾数的小数部分
- ee 是指数位存的无符号整数
- biasbias 是指数偏移量
单精度 float
float 通常是 32 位,也叫 binary32:
- 1 位符号位
- 8 位指数位
- 23 位尾数位
也就是:32=1+8+23
偏移量是:bias=127
所以 float 的值通常是:
双精度 double
double 通常是 64 位,也叫 binary64:
- 1 位符号位
- 11 位指数位
- 52 位尾数位
也就是:64=1+11+52
偏移量是:bias=1023
所以 double 的值通常是:
为什么尾数前面有个 1,但又不存它
因为正规二进制浮点数规格化后,一般都写成:
这个最高位的 1 总是存在,所以 IEEE 754 直接省略不存,叫“隐藏位”或“隐含的前导 1”。
这也是为什么:
- float 虽然只存 23 位尾数
- 但实际有效精度常说约 24 位二进制位
double 同理:
- 存 52 位
- 实际有效精度约 53 位二进制位
逐位拆一个例子:12.375
先把它转成二进制:
整数部分:
小数部分:
所以:
规格化:
于是:
- 符号位是 0,因为它是正数
- 真实指数是 3
- 尾数部分是 100011 后面补 0
如果按 float 存
指数位存的是:3+127=130
130 的二进制是:10000010
尾数位存:10001100000000000000000
所以 12.375 的 float 编码可以理解成:
- 符号位:0
- 指数位:10000010
- 尾数位:10001100000000000000000
如果按 double 存
指数位存的是:3+1023=1026
1026 的二进制是:10000000010
尾数位还是 100011,后面补更多 0 到 52 位。
特殊值也要知道
IEEE 754 不只表示普通数字,还保留了特殊编码:
- 指数全 0,尾数全 0:表示 0
- 指数全 0,尾数不全 0:非正规数
- 指数全 1,尾数全 0:无穷大
- 指数全 1,尾数不全 0:NaN
这也是为什么浮点数能表示:
- +0 和 -0
- +∞ 和 -∞
- NaN
2. 浮点数比较为什么不能直接用 ==
最核心原因只有一句:
很多十进制小数不能被二进制浮点数精确表示,所以你写出来的值和计算出来的值,可能只是“非常接近”,不是“完全一样”。
最经典例子:
不是数学错了,而是机器里存的其实是:
- 0.1 的近似值
- 0.2 的近似值
- 0.3 的近似值
而:
近似值 + 近似值 不一定正好等于 另一个近似值
为什么 0.1 不能精确表示
因为 0.1 是十进制小数,但二进制里它是无限循环的。
这和十进制里:
是一个道理。
所以机器只能截断并舍入,保存一个最接近它的值。
直接用 == 的问题
如果你写:
- 算出来一个浮点结果
- 再拿它和某个理论值直接做 ==
哪怕数学上应该相等,机器上也可能差一点点。
例如常见的思路会失败:
- a 是算出来的
- b 是直接写死的常量
- a 和 b 肉眼看一样
- 但二进制最低若干位不一样
什么时候 == 是危险的
下面这些场景都要警惕:
- 连续很多次浮点运算后再比较
- 涉及除法
- 涉及三角函数、开方、对数等数学函数
- 把不同路径算出的结果直接比较
- 把中间结果和理论常量直接比较
更合理的比较方法:看误差
最常见是绝对误差:
如果
就认为它们“足够相等”。
例如:
但只用绝对误差也不总够,因为数值尺度可能差很多。
例如:
- 比较 0.000001 和 0.000002
- 比较 1000000 和 1000000.000001
它们对误差容忍的直觉不一样。
所以更稳妥的是结合绝对误差和相对误差:
如果满足:
就认为它们足够接近。
什么时候可以直接用 ==
也不是永远不能用。以下场景可以直接用:
- 和 0 比较时,若这个 0 来自明确赋值而不是复杂计算,要具体看来源
- 两个值都来自同一份离散编码转换,没有中间浮点运算
- 比较的是无穷大或 NaN 状态时,用专门函数判断
- 你明确知道这些值是二进制可精确表示的,例如 0.5、0.25、0.125 这类
但经验上,业务代码里只要是“算出来再比较”,优先默认不要直接用 ==。
补两个常见坑
- 不要比较浮点数是否精确等于某个小数常量
- 不要用很随意的 epsilon
epsilon 不是越小越好,它要和你的业务量级匹配。
例如:
- 图形坐标容差
- 金融计算通常不建议用浮点做金额
- 物理仿真和机器学习容忍度也不同
3. float、double、long double 在项目里怎么选
先给最实用的结论:
- 默认数值计算,优先 double
- 内存和带宽很敏感、且精度够用时,用 float
- long double 只在确实需要更高精度、且你确认平台实现有效时再用
先看三者大致特点
float
- 通常 4 字节
- 精度大约 6 到 7 位十进制有效数字
- 占内存小
- 缓存友好
- 在图形、音频、神经网络、海量数组中很常见
double
- 通常 8 字节
- 精度大约 15 到 16 位十进制有效数字
- 通用数值计算最常用
- 比 float 稳妥很多
- 是很多工程代码的默认首选
long double
- 大小和精度平台相关
- 有的平台是 80 位扩展精度
- 有的平台只是 double 的别名
- 可移植性最差
- 只适合明确知道平台特性的高精度场景
项目里怎么选
场景 1:一般业务计算、工程计算、算法题、后端逻辑
优先 double。
原因:
- 精度比 float 高很多
- 大多数 CPU 对 double 支持很好
- 可以减少很多“精度不够”的意外
- 写起来最省心
简单说:
如果你没有特别理由,double 是最稳妥的默认值。
场景 2:图形、音频、游戏、大规模张量、神经网络
很多时候 float 更常见。
原因:
- 数据量极大时,float 只有 4 字节
- 更省内存
- 更省带宽
- SIMD、GPU、张量硬件往往对 float 非常友好
例如:
- 顶点坐标
- 图像像素计算
- 深度学习参数
- 音频采样处理
这类场景常常用 float,不是因为它“更数学正确”,而是因为“精度够用且性价比更高”。
场景 3:金融金额
通常不要直接用 float 或 double 存钱。
更常见做法是:
- 用整数表示最小货币单位
- 或用高精度十进制库
原因:
- 浮点不是十进制精确表示
- 金额计算通常要求精确到分甚至更高
- 舍入规则还常常有严格业务要求
场景 4:高精度科学计算
先默认 double。
如果真的不够,再考虑:
- long double
- 多精度库
- 专用高精度数值库
不要一上来就 long double,因为:
- 平台差异大
- 性能未必理想
- 有时收益没有你想象中大
场景 5:嵌入式和性能极敏感平台
要看硬件。
- 有的硬件对 float 支持很好,但 double 很弱
- 有的硬件没有强 FPU,浮点整体都贵
- 这种场景要以目标平台测试结果为准
所以在嵌入式里,类型选择不能只看语言规则,还要看芯片。
一个很实用的选型表
| 场景 | 推荐类型 |
|---|---|
| 普通数值计算 | double |
| 图形、音频、海量张量 | float |
| 金额、精确十进制 | 不建议 float/double |
| 超高精度数值 | 先 double,不够再 long double 或多精度库 |
| 平台相关高性能嵌入式 | 依据硬件测试决定 |
关于性能怎么理解
不要简单背“float 一定比 double 快”或者“int 一定比浮点快”。
更实际的是:
- 桌面 CPU 上,double 往往已经很快
- 大数组处理时,double 更吃内存带宽
- GPU/向量化场景里,float 常常更划算
- 混合类型运算可能带来转换成本
- 真要优化,最后还是 benchmark 说话
最后给一套很实用的工程建议
- 默认用 double 作为通用浮点类型
- 数据规模特别大时,再认真评估 float
- 不要为了“可能更快”盲目把 double 改成 float
- 不要指望 long double 解决所有精度问题
- 金额和高精度十进制业务,优先考虑整数或专用库
- 浮点结果比较时,优先用误差比较,不要随便 ==
一句话总记忆
- IEEE 754 就是符号位、指数位、尾数位的二进制科学计数法
- 浮点不能直接乱用 ==,因为很多值只是近似表示
- 项目里默认优先 double,海量数据和图形场景常用 float,long double 只在明确需要时再上
