定点数与浮点数区别

定点数与浮点数区别最近做 HDR 时 经常要用 NV 提供的 16 位纹理 它的说明书 16 位能达到 24 位的精度 就很奇怪 一直搞不懂浮点数的精度怎么算的 今天认真看了一下 IEEEfloatpoi 的标准 终于明白是什么了 1

原文地址为: 定点数与浮点数区别

定点数表达法的缺点在于其形式过于僵硬,固定的小数点位置决定了固定位数的整数部分和小数部分,不利于同时表达特别大的数或者特别小的数。最终,绝大多数现代的计算机系统采纳了所谓的浮点数表达方式。这种表达方式利用科学计数法来表达实数,即用一个尾数(Mantissa ),一个基数(Base),一个指数(Exponent)以及一个表示正负的符号来表达实数。比如 123.45 用十进制科学计数法可以表达为 1.2345 × 102 ,其中 1.2345 为尾数,10 为基数,2 为指数。浮点数利用指数达到了浮动小数点的效果,从而可以灵活地表达更大范围的实数。

提示: 尾数有时也称为有效数字(Significand)。尾数实际上是有效数字的非正式说法。

同样的数值可以有多种浮点数表达方式,比如上面例子中的 123.45 可以表达为 12.345 × 101,0.12345 × 103 或者 1.2345 × 102。因为这种多样性,有必要对其加以规范化以达到统一表达的目标。规范的(Normalized)浮点数表达方式具有如下形式:

±d.dd…d × β e , (0 ≤ d i < β)

其中 d.dd…d 即尾数,β 为基数,e 为指数。尾数中数字的个数称为精度,在本文中用 p 来表示。每个数字 d 介于 0 和基数之间,包括 0。小数点左侧的数字不为 0。

基于规范表达的浮点数对应的具体值可由下面的表达式计算而得:

±(d 0 + d 1β-1 + … + d p-1β-(p-1))β e , (0 ≤ d i < β)

对于十进制的浮点数,即基数 β 等于 10 的浮点数而言,上面的表达式非常容易理解,也很直白。计算机内部的数值表达是基于二进制的。从上面的表达式,我们可以知道,二进制数同样可以有小数点,也同样具有类似于十进制的表达方式。只是此时 β 等于 2,而每个数字 d 只能在 0 和 1 之间取值。比如二进制数 1001.101 相当于 1 × 2 3 + 0 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 0 × 2-2 + 1 × 2-3,对应于十进制的 9.625。其规范浮点数表达为 1.001101 × 23。

注意: Java 平台还支持该标准定义的两种扩展格式,即 float-extended-exponent 和 double-extended-exponent 扩展格式。这里将不作介绍,有兴趣的读者可以参考相应的参考资料。

在 IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为”浮点”)就可以表达给定的数值了。具体的格式参见下面的图例:

 

在上面的图例中,第一个域为符号域。其中 0 表示数值为正数,而 1 则表示负数。

第二个域为指数域,对应于我们之前介绍的二进制科学计数法中的指数部分。其中单精度数为 8 位,双精度数为 11 位。以单精度数为例,8 位的指数为可以表达 0 到 255 之间的 255 个指数值。但是,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上一个偏差(Bias)值作为保存在指数域中的值,单精度数的偏差值为 127,而双精度数的偏差值为 1023。比如,单精度的实际指数值 0 在指数域中将保存为 127;而保存在指数域中的 64 则表示实际的指数值 -63。 偏差的引入使得对于单精度数,实际可以表达的指数值的范围就变成 -127 到 128 之间(包含两端)。我们不久还将看到,实际的指数值 -127(保存为 全 0)以及 +128(保存为全 1)保留用作特殊值的处理。这样,实际可以表达的有效指数范围就在 -127 和 127 之间。在本文中,最小指数和最大指数分别用 emin 和 emax 来表达。

图例中的第三个域为尾数域,其中单精度数为 23 位长,双精度数为 52 位长。除了我们将要讲到的某些特殊值外,IEEE 标准要求浮点数必须是规范的。这意味着尾数的小数点左侧必须为 1,因此我们在保存尾数的时候,可以省略小数点前面这个 1,从而腾出一个二进制位来保存更多的尾数。这样我们实际上用 23 位长的尾数域表达了 24 位的尾数。比如对于单精度数而言,二进制的 1001.101(对应于十进制的 9.625)可以表达为 1.001101 × 23,所以实际保存在尾数域中的值为 000000000000,即去掉小数点左侧的 1,并用 0 在右侧补齐。

值得注意的是,对于单精度数,由于我们只有 24 位的指数(其中一位隐藏),所以可以表达的最大指数为 224 – 1 = 16,777,215。特别的,16,777,216 是偶数,所以我们可以通过将它除以 2 并相应地调整指数来保存这个数,这样 16,777,216 同样可以被精确的保存。相反,数值 16,777,217 则无法被精确的保存。由此,我们可以看到单精度的浮点数可以表达的十进制数值中,真正有效的数字不高于 8 位。事实上,对相对误差的数值分析结果显示有效的精度大约为 7.22 位。参考下面的示例:

提示: 这里采用的浮点数舍入规则有时被称为舍入到偶数(Round to Even)。相比简单地逢一半则进的舍入规则,舍入到偶数有助于从某些角度减小计算中产生的舍入误差累积问题。因此为 IEEE 标准所采用。

首先我们来看看事情简单的一面,从浮点数变换到实数。理解了浮点数的格式,做这个练习并不难。假定我们有一个 32 位的数据,用十六进制表示为 0xC0B40000,并且我们知道它实际上是一个单精度的浮点数。为了得到该浮点数实际表达的实数,我们首先将它变换为二进制形式:

1  000000000000符号域 1 意味着负数;指数域为 129 意味着实际的指数为 2 (减去偏差值 127);尾数域为 01101 意味着实际的二进制尾数为 1.01101 (加上隐含的小数点前面的 1)。所以,实际的实数为:

首先,将小数点左侧的整数部分变换为其二进制形式,9 的二进制性形式为 1001。处理小数部分的算法是将我们的小数部分乘以基数 2,记录乘积结果的整数部分,接着将结果的小数部分继续乘以 2,并不断继续该过程:

因为是负数,所以符号域为 1。指数为 3,所以指数域为 3 + 127 = 130,即二进制的 。尾数省略掉小数点左侧的 1 之后为 001101,右侧用零补齐。最终结果为:

1 000000000000最后可以将浮点数形式表示为十六进制的数据如下:

很简单?等等!你可能已经注意到了,在上面这个我们有意选择的示例中,不断的将产生的小数部分乘以 2 的过程掩盖了一个事实。该过程结束的标志是小数部分乘以 2 的结果为 1,不难想象,很多小数根本不能经过有限次这样的过程而得到结果(比如最简单的 0.1)。我们已经知道浮点数尾数域的位数是有限的,为此,浮点数的处理办法是持续该过程直到由此得到的尾数足以填满尾数域,之后对多余的位进行舍入。换句话说,除了我们之前讲到的精度问题之外,十进制到二进制的变换也并不能保证总是精确的,而只能是近似值。事实上,只有很少一部分十进制小数具有精确的二进制浮点数表达。再加上浮点数运算过程中的误差累积,结果是很多我们看来非常简单的十进制运算在计算机上却往往出人意料。这就是最常见的浮点运算的”不准确”问题。参见下面的 Java 示例:

System.out.print(“34.6-34.0=” + (34.6f-34.0f));这段代码的输出结果如下:

34.6-34.0=0.产生这个误差的原因是 34.6 无法精确的表达为相应的浮点数,而只能保存为经过舍入的近似值。这个近似值与 34.0 之间的运算自然无法产生精确的结果。

我们已经知道,指数域实际可以表达的指数值的范围为 -127 到 128 之间(包含两端)。其中,值 -127(保存为 全 0)以及 +128(保存为全 1)保留用作特殊值的处理。本节将详细 IEEE 标准中所定义的这些特殊值。

浮点数中的特殊值主要用于特殊情况或者错误的处理。比如在程序对一个负数进行开平方时,一个特殊的返回值将用于标记这种错误,该值为 NaN(Not a Number)。没有这样的特殊值,对于此类错误只能粗暴地终止计算。除了 NaN 之外,IEEE 标准还定义了 ±0,±∞ 以及非规范化数(Denormalized Number)。

对于单精度浮点数,所有这些特殊值都由保留的特殊指数值 -127 和 128 来编码。如果我们分别用 emin 和 emax 来表达其它常规指数值范围的边界,即 -126 和 127,则保留的特殊指数值可以分别表达为 emin – 1 和 emax + 1; 。基于这个表达方式,IEEE 标准的特殊值如下所示:

 

其中 f 表示尾数中的小数点右侧的(Fraction)部分。第一行即我们之前介绍的普通的规范化浮点数。随后我们将分别对余下的特殊值加以介绍。

定制的 NaN 值,可以通过 Float.isNaN() 方法判定其为 NaN,但是它和 Float.NaN 常量却不相等。实际上,所有的 NaN 值都是无序的。数值比较操作符 <,<=,> 和 >= 在任一操作数为 NaN 时均返回 false。等于操作符 == 在任一操作数为 NaN 时均返回 false,即使是两个具有相同位模式的 NaN 也一样。而操作符 != 则当任一操作数为 NaN 时返回 true。这个规则的一个有趣的结果是 x!=x 当 x 为 NaN 时竟然为真。

可以产生 NaN 的操作如下所示:

 

此外,任何有 NaN 作为操作数的操作也将产生 NaN。用特殊的 NaN 来表达上述运算错误的意义在于避免了因这些错误而导致运算的不必要的终止。比如,如果一个被循环调用的浮点运算方法,可能由于输入的参数问题而导致发生这些错误,NaN 使得 即使某次循环发生了这样的错误,也可以简单地继续执行循环以进行那些没有错误的运算。你可能想到,既然 Java 有异常处理机制,也许可以通过捕获并忽略异常达到相同的效果。但是,要知道,IEEE 标准不是仅仅为 Java 而制定的,各种语言处理异常的机制不尽相同,这将使得代码的迁移变得更加困难。何况,不是所有语言都有类似的异常或者信号(Signal)处理机制。

注意: Java 中,不同于浮点数的处理,整数的 0 除以 0 将抛出 java.lang.ArithmeticException 异常。

无穷和除 NaN 以外的其它浮点数一样是有序的,从小到大依次为负无穷,负的有穷非零值,正负零(随后介绍),正的有穷非零值以及正无穷。除 NaN 以外的任何非零值除以零,结果都将是无穷,而符号则由作为除数的零的符号决定。

回顾我们对 NaN 的介绍,当零除以零时得到的结果不是无穷而是 NaN 。原因不难理解,当除数和被除数都逼近于零时,其商可能为任何值,所以 IEEE 标准决定此时用 NaN 作为商比较合适。

实际上,零保存为尾数域为全为 0,指数域为 emin – 1 = -127,也就是说指数域也全为 0。考虑到符号域的作用,所以存在着两个零,即 +0 和 -0。不同于正负无穷之间是有序的,IEEE 标准规定正负零是相等的。

零有正负之分,的确非常容易让人困惑。这一点是基于数值分析的多种考虑,经利弊权衡后形成的结果。有符号的零可以避免运算中,特别是涉及无穷的运算中,符号信息的丢失。举例而言,如果零无符号,则等式 1/(1/x) = x 当x = ±∞ 时不再成立。原因是如果零无符号,1 和正负无穷的比值为同一个零,然后 1 与 0 的比值为正无穷,符号没有了。解决这个问题,除非无穷也没有符号。但是无穷的符号表达了上溢发生在数轴的哪一侧,这个信息显然是不能不要的。零有符号也造成了其它问题,比如当 x=y 时,等式1/x = 1/y 在 x 和 y 分别为 +0 和 -0 时,两端分别为正无穷和负无穷而不再成立。当然,解决这个问题的另一个思路是和无穷一样,规定零也是有序的。但是,如果零是有序的,则即使 if (x==0) 这样简单的判断也由于 x 可能是 ±0 而变得不确定了。两害取其轻者,零还是无序的好。

现在我们看看这两个浮点数的差值。不难得出,该差值为 0.0001 × 2-125,表达为规范浮点数则为 1.0 × 2-129。问题在于其指数大于允许的最小指数值,所以无法保存为规范浮点数。最终,只能近似为零(Flush to Zero)。这中特殊情况意味着下面本来十分可靠的代码也可能出现问题:

为了解决此类问题,IEEE 标准中引入了非规范(Denormalized)浮点数。规定当浮点数的指数为允许的最小指数值,即 emin 时,尾数不必是规范化的。比如上面例子中的差值可以表达为非规范的浮点数 0.001 × 2-126,其中指数 -126 等于 emin。注意,这里规定的是”不必”,这也就意味着”可以”。当浮点数实际的指数为 emin,且指数域也为 emin 时,该浮点数仍是规范的,也就是说,保存时隐含着一个隐藏的尾数位。为了保存非规范浮点数,IEEE 标准采用了类似处理特殊值零时所采用的办法,即用特殊的指数域值 emin – 1 加以标记,当然,此时的尾数域不能为零。这样,例子中的差值可以保存为 000000000000000000000(0x),没有隐含的尾数位。

转载请注明本文地址: 定点数与浮点数区别



版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/230646.html原文链接:https://javaforall.net

(0)
上一篇 2026年2月11日 下午1:01
下一篇 2026年2月11日 下午1:22


相关推荐

  • pic单片机c语言 程序,PIC单片机C语言编程教程(1)

    pic单片机c语言 程序,PIC单片机C语言编程教程(1)原代码调试 使用 MPLAB IDE 内的调试工具 ICE2000 ICD2 和软件模拟器都可以实现原代码级的程序调试 非常方便 首先必须在你的计算机中安装 PICC 编译器 无论是完全版还是学习版都可以和 MPLAB IDE 挂接 安装成功后可以进入 IDE 选择菜单项 ProjectSetLa 打开语言工具挂接设置对话框 如图 11 1 所示 C3 82 C

    2025年11月17日
    5
  • 试题 H: 人物相关性分析 第十届蓝桥杯

    试题 H: 人物相关性分析 第十届蓝桥杯

    2021年7月4日
    72
  • redis淘汰策略

    redis淘汰策略redis 默认的最大的内存设置为 0 相当于基于物理机的最大值主要是 4 种算法 针对不同的 key 形成的策略 算法 key LRU 是 LeastRecentl 的缩写 也就是表示最近很少使用 也可以理解成最久没有使用 也就是说当内存不够的时候 每次添加一条数据 都需要抛弃一条最久时间没有使用的旧数据 标准的 LRU 算法为了降低查找和删除元素的时间复杂度 一般采用 Hash 表和双向链表结合的数据结构 hash 表可以赋予链表快速查找到某个 key 是否存在链表中 同时可以快速删除 添加节点 如图所示

    2026年3月26日
    3
  • java的tgz解压工具类

    java的tgz解压工具类前言之前在代码上一直使用的是对 zip 的解压 最近对接方居然使用了 tgz 的压缩包 在网上找了一个工具类 在项目测试 使用 直接贴上 tgz 解压代码 publicclassP publicstatic SIZE 2048 publicstatic String args throws

    2026年3月18日
    3
  • 常见的7种排序算法

    常见的7种排序算法1、冒泡排序最简单的一种排序算法。假设长度为n的数组arr,要按照从小到大排序。则冒泡排序的具体过程可以描述为:首先从数组的第一个元素开始到数组最后一个元素为止,对数组中相邻的两个元素进行比较,如果位于数组左端的元素大于数组右端的元素,则交换这两个元素在数组中的位置,此时数组最右端的元素即为该数组中所有元素的最大值。接着对该数组剩下的n-1个元素进行冒泡排序,直到整个数组有序排列。算法的时间复…

    2022年7月26日
    8
  • Android面试题:Handler

    Android面试题:Handler1 一个线程有几个 Handler 答 一个线程可以有多个 Handler 2 一个线程有几个 Looper 如何保证 答 一个线程只能有 1 个 Looper Looper 对象通过 ThreadLocalM 保存 ThreadLocalM 保存键值对 该键值对的键为 ThreadLocal 对象 一旦 ThreadLocalM 中已经存在 Looper 对象 再次创建 Looper 对象会抛出异常 3 Handler 内存泄漏原因 为什么其他内部类没有这个问题 答 非静态内部类或者匿名内部类 默认持有

    2026年3月27日
    3

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号