zoukankan      html  css  js  c++  java
  • 浮点数精度丢失问题

    C#中的浮点数,分单精度(float)和双精度(double):

    floatSystem.Single 的别名,介于 -3.402823e38 和 +3.402823e38 之间的32位数字,符合二进制浮点算法的 IEC 60559:1989 (IEEE 754) 标准;

    doubleSystem.Double 的别名,介于 -1.79769313486232e308 和 +1.79769313486232e308 之间的64位数字,符合二进制浮点算法的 IEC 60559:1989 (IEEE 754) 标准;

    我们知道,计算机只认识 0 和 1,所以数值都是以二进制的方式储存在内存中的。

    (对于人脑和计算机哪个聪明,个人更倾向于选择人脑,计算机只是计算得快,而且不厌其烦而已!)

    所以要知道数值在内存中是如何储存的,需先将数值转为二进制(这里指在范围内的数值)

    根据 IEEE 754 标准,任意一个二进制浮点数 V 均可表示为:V = (-1 ^ s) * M * (2 ^ e)

    其中 s ∈ {0, 1};M ∈ [1, 2);e 表示偏移指数。

    以 198903.19(10) 为例,先转成二进制的数值为:110000100011110111.0011000010100011(2)(截取 16 位小数),采用科学记数法等于 1.100001000111101110011000010100011 * (2 ^ 17)(整数位是 1),即 198903.19(10) = (-1 ^ 0) * 1.100001000111101110011000010100011 * (2 ^ 17)。

    整数部分可采用 "除2取余法",小数部分可采用 "乘2取整法"。

    从结果可以看出,小数部分 0.19 转为二进制后,小数位数超过 16 位(我已经手算到小数点后 32 位都还没算完,其实这个位数是无穷尽的)

    由于无法得到完全正确的数值,这里就引申出浮点数精度丢失的问题:

    /* 程序段1 */
    float num_a = 198903.19f;
    float num_b = num_a / 2;
    Console.WriteLine(num_a);
    Console.WriteLine(num_b);

    这段程序代码,我们预想中正确的结果应该是:198903.19 和 99451.595。

    但结果居然是!!!原因下面将讲到 ...

    这里介绍另一种转小数部分的方法,有兴趣可以看下:

    假如结果要求精确到 N 位小数,那么只需要将小数部分乘以 2 的 N 次方(例如 N = 16,0.19 * (2 ^ 16),得到 12451.84)。

    取整数部分(12451),按整数的方法转为二进制,得到 11000010100011,不足 N 位在高位用 0 补足。

    结果 0.19 精确到 16 位后,用二进制表示为 0.0011000010100011。

    可以看出,若是小数部分乘以 2 的 N 次方后,可以得到一个整数,那么这个小数可以用二进制精确表示,否则则不可以。

    (原理很简单,根据二进制小数位转十进制的方法,反推回去就可以得到这个结果)

    在内存中,float 和 double 的储存格式是一致的,只是占用的空间大小不同。

    float 总共占用 32 位:

    从左往右,第 1 位是符号位,占 1 位;第 2-9 位是指数位,占 8 位;第 10-32 位是尾数位,占 23 位。

    double 总共占用 64 位,从左往右第 1 位也是符号位,占 1 位;第 2-12 位是指数位,占 11 位;第 13-64 位是尾数位,占 52 位。

    其中,符号位(即上文的 s,下同),0 代表正数,1 代表负数。

    对于 float,8位指数位的值范围为 0-255(10),由于指数(即上文的 e,下同)可正可负,而指数位的值是一个无符号整数。根据标准规定,储存时采用偏移值(偏移值为127)的方法,储存值为指数 + 127。例如 0111 0011(2) 表示指数 -12(10)((-12) + 127 = 115),1000 1011(2) 表示指数 12(10)(12+ 127 = 139)

    {

      另外,IEEE 754 规定(同样适用于 double):

      当指数全为 0 时,如果尾数全为 0,表示 ±0(正负取决于符号位),如果尾数不全为 0,计算时指数等于 -126,尾数不加上第一位的1,而是还原为 0.xxxxxx 的小数,表示更接近 0 的小数;

      当指数全为 1 时,如果尾数全为 0,表示 ±无穷大(正负取决于符号位),如果尾数不全为 0,表示这不是一个数(NaN)。

      资料来自非规约形式的浮点数

    }

    同样的,对于 double,11位指数位,储存时采用的偏移值为 1023。

    尾数位,由于所有数值均可以转换成 1.xxx * (2 ^ N)(此处暂时忽略精度问题),所以尾数部分只保存小数部分(最高位的 1 不存入内存,提高 1 个位的精度)

    以 float 198903.19 为例,二进制为 1.100001000111101110011000010100011 * (2 ^ 17);

    数值为正数,符号位是 0;

    指数是 17,保存为 144(17 + 127 = 144),即 10010000(共 8 位,不足 8 位在高位用 0 补足)

    小数位是 10000100011110111001100(截取 23 位)

    最终得到:01001000 01000010 00111101 11001100,按字节倒序顺序,转为十六进制就是:CC 3D 42 48

    float f_num = 198903.19f;
    var f_bytes = BitConverter.GetBytes(f_num);
    Console.WriteLine("float: 198903.19");
    Console.WriteLine(BitConverter.ToString(f_bytes));
    Console.WriteLine(string.Join(" ", f_bytes.Select(i => Convert.ToString(i, 2).PadLeft(8, '0'))));

    同样的格式,double 198903.19 最终得到:01000001 00001000 01000111 10111001 10000101 00011110 10111000 01010010(最后两位结果为什么是 10 而不是 01,请参考浮点数的舍入,按字节倒序顺序,转为十六进制就是:52 B8 1E 85 B9 47 08 41

    double d_num = 198903.19d;
    var d_bytes = BitConverter.GetBytes(d_num);
    Console.WriteLine("double: 198903.19");
    Console.WriteLine(BitConverter.ToString(d_bytes));
    Console.WriteLine(string.Join(" ", d_bytes.Select(i => Convert.ToString(i, 2).PadLeft(8, '0'))));

    回到精度丢失的问题,由于小数位无法算尽,内存用截取精度的方式储存了转换后的二进制,这导致保存的结果并非是完全正确的数值。

    看回 程序段1 的例子,

    num_a 在内存中其实是保存为:01001000 01000010 00111101 11001100,换算成十进制就是:198903.1875;

    num_b 在内存中其实是保存为:01000111 11000010 00111101 11001100,换算成十进制就是:99451.59375;

    先看 num_b,由于 num_a 在内存中储存的值已经是不正确的,那么再利用其进行计算,得到的结果 99.9% 也会是不正确的。所以 num_b 的结果并不是我们想要的 99451.595。

    然后为什么 198903.1875 会变成 198903.2,而 99451.59375 会变成 99451.595 呢?我们知道,内存中确实是储存了 198903.1875 和 99451.59375 这两个值,那么就只有可能是在输出的时候做了变动。其实这是微软做的小把戏,我们有句俗话说"以毒攻毒",大概就这个意思,既然储存的已经是不正确的数值,那么在输出的时候,会智能地猜测判断原先正确的数值是什么,然后输出猜测的那个值,说不定就真的猜中了呢!

    (之前看过的一篇文章写的,忘了地址,大概就这个意思。如果不是因为这个原因,大家就当饭后娱乐吧。)

    如果有写错的地方,请帮忙指正。谢谢 ...

  • 相关阅读:
    王一恒《跨部门沟通与协作》讲座学习笔记(图文)
    DotNetBar.Bar菜单的使用
    android app 架构设计01
    crm高速开发之OrganizationService
    《Java程序设计》第16周周五:数据库连接 与 随机数的使用
    hdu 1058 Humble Numbers
    iOS开发之获取沙盒路径
    [Swift通天遁地]七、数据与安全-(2)对XML和HTML文档的快速解析
    [Swift]LeetCode394. 字符串解码 | Decode String
    [Swift]LeetCode393. UTF-8 编码验证 | UTF-8 Validation
  • 原文地址:https://www.cnblogs.com/SugarLSG/p/3534248.html
Copyright © 2011-2022 走看看