深入了解浮点数在计算机中的存储方式和运算
浮点数我们都很熟悉但它在计算机中是怎么存储和运算的它和int类型数据运算哪个速度快带着这些疑问博主又重新学习了浮点数相关的知识以前学过但有些知识点有点模糊了先给结论浮点数不是像int那样“直接存一个值”而是按科学计数法的二进制版本来存。最常见的是 IEEE 754 标准格式本质上拆成三部分符号位指数位尾数位它和int比谁更快没有绝对答案。通常可以这样记纯整数加减、比较、位运算int更直接现代 CPU 上浮点加减乘也很快不一定明显慢很多混合int和浮点运算会有类型转换通常更慢一点真正影响性能的很多时候不只是算术本身还有缓存、内存带宽、是否有 FPU、是否能 SIMD 并行一、浮点数是怎么存的最常见的浮点类型float通常 32 位double通常 64 位long double实现相关不同平台可能不同以最常见的 IEEE 754 为例1. float 的常见布局1 位符号位8 位指数位23 位尾数位也就是3218232. double 的常见布局1 位符号位11 位指数位52 位尾数位也就是6411152二、浮点数的值怎么还原以正规浮点数为例值大致是(−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.300000000000000044410说明浮点数存的是近似值运算结果也可能有误差不能随便用比较浮点数十二、一个混合运算例子#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/30.3333…1/30.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.100000000000000005550.200000000000000011100.300000000000000044410所以浮点数比较时通常不要直接用 而是看误差是否足够小#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 的存储结构和性能对比先看最核心的结构区别。类型常见大小存储方式能否精确表示整数能否表示小数典型用途int4 字节补码整数可以在范围内精确不行计数、下标、离散值float4 字节IEEE 754 单精度浮点只能精确表示一部分整数可以但有误差图形、实时计算、对内存敏感的数值double8 字节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 和 floatdouble 更吃内存带宽批量遍历时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 是指数偏移量单精度 floatfloat 通常是 32 位也叫 binary321 位符号位8 位指数位23 位尾数位也就是321823偏移量是bias127所以 float 的值通常是双精度 doubledouble 通常是 64 位也叫 binary641 位符号位11 位指数位52 位尾数位也就是6411152偏移量是bias1023所以 double 的值通常是为什么尾数前面有个 1但又不存它因为正规二进制浮点数规格化后一般都写成这个最高位的 1 总是存在所以 IEEE 754 直接省略不存叫“隐藏位”或“隐含的前导 1”。这也是为什么float 虽然只存 23 位尾数但实际有效精度常说约 24 位二进制位double 同理存 52 位实际有效精度约 53 位二进制位逐位拆一个例子12.375先把它转成二进制整数部分小数部分所以规格化于是符号位是 0因为它是正数真实指数是 3尾数部分是 100011 后面补 0如果按 float 存指数位存的是3127130130 的二进制是10000010尾数位存10001100000000000000000所以 12.375 的 float 编码可以理解成符号位0指数位10000010尾数位10001100000000000000000如果按 double 存指数位存的是3102310261026 的二进制是10000000010尾数位还是 100011后面补更多 0 到 52 位。特殊值也要知道IEEE 754 不只表示普通数字还保留了特殊编码指数全 0尾数全 0表示 0指数全 0尾数不全 0非正规数指数全 1尾数全 0无穷大指数全 1尾数不全 0NaN这也是为什么浮点数能表示0 和 -0∞ 和 -∞NaN2. 浮点数比较为什么不能直接用 最核心原因只有一句很多十进制小数不能被二进制浮点数精确表示所以你写出来的值和计算出来的值可能只是“非常接近”不是“完全一样”。最经典例子不是数学错了而是机器里存的其实是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 这类但经验上业务代码里只要是“算出来再比较”优先默认不要直接用 。补两个常见坑不要比较浮点数是否精确等于某个小数常量不要用很随意的 epsilonepsilon 不是越小越好它要和你的业务量级匹配。例如图形坐标容差金融计算通常不建议用浮点做金额物理仿真和机器学习容忍度也不同3. float、double、long double 在项目里怎么选先给最实用的结论默认数值计算优先 double内存和带宽很敏感、且精度够用时用 floatlong 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海量数据和图形场景常用 floatlong double 只在明确需要时再上

相关新闻

最新新闻

日新闻

周新闻

月新闻