Go 之父说:不懂浮点数不配当码农

栏目: Go · 发布时间: 5年前

Go 之父说:不懂浮点数不配当码农

Go 之父说:不懂浮点数不配当码农

所以要赶紧补充一些高大上的浮点数知识吧

浮点数很重要

Go语言之父,Rob Pike大神曾经在微博吐槽过:不能掌握正则表达式或浮点数就不配当码农!

虽然原文已经被删除了(大神也有害怕的时候),还好我已经存档了:

You should not be permitted to write production code if you do not have an journeyman license in regular expressions or floating point math.

关于正则表达式已经有很多权威著作或入门教程(但是很多主流的编程语言的实现居然也藏了不少烂算法,性能被几十年前ed指数级吊打)。

但是讨论浮点数的资料则比较少。一般的编程图书也不会深入讨论浮点数。但是这并不代表浮点数不重要。实际上IEEE754之父,William Kahan正是因为浮点数标准化的工作获得了图灵奖。

为什么叫浮点数

人的生命是有限的,计算机的内存也是有限的。在主流的编程语言中,浮点数一般有32bit和64bit两种,分别对应单精度和双精度浮点数(有些地方还有半精度的浮点数),也就是说一个32bit的内存只能表达 2^32 种状态(大概40亿)!

但是实际上数从无穷小到无穷大范围异常的广泛。一个阿伏伽德罗常数要写成60221407600000000000000,十进制都要24位,32bit的二进制位根本不够用。此外像原子半径之类的数又非常之小,前面要用一垃圾0来填充。

为了少写很多0(因为纸张有限,内存也只有有限的32比特),懒惰的先贤们发明了科学记数法来表示很大或很小的数。科学记数法的思路就是用较少的指数来表示很大或很小数的系数。但是光有科学记数法还不行,比如 60221407600000000000000*10^0 依然没有节省纸张。只有规范化的科学记数法才能少写垃圾0,比如 6.02*10^23 看起来就很清爽了。而规范化的科学记数法中,有效位部分的小数点是随着指数发生变化的。

浮点数也是采用类似规范化的科学记数法的思路。而IEEE754是浮点数格式的国际标准,目前主流的编程语言都是采用这个标准。

浮点数的布局

这是 C语言 表示的float浮点数内存布局:

// Little endian
union ieee754_float {
    float f;

    /* This is the IEEE 754 single-precision format.  */
    struct {
        unsigned int mantissa:23;
        unsigned int exponent:8;
        unsigned int negative:1;
    } ieee;

    /* This format makes it easier to see if a NaN is a signalling NaN.  */
    struct {
        unsigned int mantissa:22;
        unsigned int quiet_nan:1;
        unsigned int exponent:8;
        unsigned int negative:1;
    } ieee_nan;
};

其中最低的23it是mantissa对应有效数字,然后是8bit的exponent表示指数,最高位的1bit表示符号位。需要注意的是指数部分采用移码表示,也就是exponent作为无符号数减去127得到的最终的指数。

为何float32只有6个精度

因为有效数字mantissa部分是23bit,而 1<<23 对应十进制的8388608,不能完整表示全部的7个十进制位,因此就只剩下6个有效数字了。因此 fmt.Printf 中的 %f 对应float32默认就只打印6个十进制数(因为多打印就超出mantissa的表示范围,不能准确表达)。

浮点数的诡异:浮点数有2个0

知道了浮点数的布局,就自然可以理解为什么浮点数会有2个0。因为浮点数有一个独立的符号位,只要吧0.0的符号位设置为负数就可以得到一个负数0:

fmt.Println(math.Float32frombits(0))     // 0
fmt.Println(math.Float32frombits(1<<31)) // -0

上面的代码通过在0的基础之上加 1<<31 将符号位设置为1,这样就得到了负0。

因此用浮点数作为map的Key时,可能会遇到一些诡异的事件。比如-0能取到0对应的值吗?

浮点数的诡异:没有0.3

第一次看到这个标题,有人可能有异议。证据在这里:

fmt.Println(0.3) // 打印出了0.3

但是作为一个杠精,我要说 fmt.Println 代码是有问题的,它可能作弊了,比如:

func fmt.Println(x) {
    if x == 0.3 {
        println("0.3")
    }
}

fmt.Println 内部打印的是字符串“0.3”,不是浮点数的0.3!

实际上fmt包真的是作弊了(而且作弊的算法也成了一门学问),不信看下面这个代码:

func main() {
    fmt.Printf("%f\n", 0.3)
    fmt.Printf("%.10f\n", 0.3)
    fmt.Printf("%.20f\n", 0.3)
}

// Output:
// 0.300000
// 0.3000000000
// 0.29999999999999998890

每次输出的结果都不一样,这不是作弊是什么?fmt包作弊打印0.3的原因是因为IEEE754中不存在0.3这个数!

不相信的话可以换成0.5试试:

func main() {
    fmt.Printf("%f\n", 0.5)
    fmt.Printf("%.10f\n", 0.5)
    fmt.Printf("%.20f\n", 0.5)
}

// 0.500000
// 0.5000000000
// 0.50000000000000000000

输出0.5时,就不会因为精度的变化导致输出结果发生抖动。

对于 Go 语言中,常量和变量的运算规则是不同的,因此 0.1+0.2 换成变量就会发生变化:

func main() {
    var a = 0.1
    var b = 0.2
    fmt.Println(0.1+0.2, a+b)
}
// 0.3 0.30000000000000004

第一个输出是0.3,第二个居然不是0.3。还好我们已经知道没有0.3这个浮点数,因此第一个 0.1+0.2 的结果也不是0.3。 不能表示0.3的原因是因为计算机采用的是二进制的科学记数法,而0.3无法通过用2的不同指数的状态组成得到。

不仅没有0.3,浮点数中缺少的数字多了去了。根据抽屉原理,float32只能表示2的32次方个状态,也就是40多亿个数。但是float32的值域可是已经大大超出了40亿的范围,因此里面必然有很多数在吃空饷。

无穷有正负

无穷在浮点数中对应一个特殊的数(或者说指数值最大的那种0)。参考前面的浮点数内存布局,指数有8个bit表示,最大的指数表示是255。

下面我们构造一个指数部分是255的0:

fmt.Println(math.Float32frombits(255<<23)) // +Inf    

这样得到的就是一个正的无穷(255最大的指数表示穷,0有效位表示无)。

有了正无穷,得到负无穷就比较容易了。只要加一个符号标志位即可:

fmt.Println(math.Float32frombits(255<<23 + 1<<31)) // -Inf

用科学记数法表示是这样: 0.0*2^(255-127)

Nan不是一个数,它表示的是一种bit模式

在IEEE754浮点数的指数值域中255是最重要的一个,因为无穷对应的指数就是255。在无穷中除了指数是255之外,有效位部分是0。那么问题来了,指数为255,有效位不是0的是啥玩意?

可以跑代码看看:

fmt.Println(math.Float32frombits(255<<23 + 1)) // NaN
fmt.Println(math.Float32frombits(255<<23 + 2)) // NaN

它们都是NaN,也就是Not-a-Number,非数不是数。NaN不是一个非数,而是一类非数。

Nan有个重要的特性,就是自己和自己都不相等(想想如果用它作为map的key会有什么效果)。Nan是非法的运算得来的,比如 sqrt(-1)0/0 都是非数。

浮点数不满足结合率

浮点数不满足结合率,比如 a+(b+c)(a+b)+c 不等价。具体原因和开头的 x=x+1 方程类似。

如果 x=x+1 成立,但是 x=x+2 并不一定成立。如果 x!=x+2 ,那么显然它和 x=(x+1)+1 结果是不等价的。

四舍五入是错误的

四舍五入分为2个部分,四舍还是四舍,但是五入就不一定是五入了。还存在五舍的情形。五作为一个绝对中间的位置,凭什么总是要五入(借钱的人和还钱的人肯定也有不同的看法)?

那什么时候需要五舍?根据计算的结果长得是否漂亮,选择五舍或五入。

指数为什么要采用移码

在早年间,浮点数运算芯片是一个奢侈的玩意。码农们都不做浮点数运算。但是运气也有背的时候,比如需要给一个浮点数表示的数组排序。

正如凌凌漆所言,IEEE754并非浪得虚名。即使没有浮点数芯片,码农们依然可以快速给浮点数数组排序:将浮点数数组当中整数数字进行 排序 就可以了。

为何?因为浮点数数组有序的话,那么同样bit模式的整数数组依然是有序的。其中第一个原因是符号位保证有序,第二个原因是指数采用移码保证大指数较大,最后几十有效数字部分比较小数点部分大数字。

解浮点数方程: x+1=x

不是纯数学意义上的方程, 对应计算机的一个浮点数问题:

if((float)(x+1.0) == (float)(x)) { x = ? }

简单分析, ieee754中float采用23bit表示有效位, 再加省略的1, 共有24bit.

当结果超出24bit时, 小数部分被被丢失:

var a float32 = 1 << 24
var b float32 = a+1

fmt.Println(math.Float32bits(a)) // 1266679808
fmt.Println(math.Float32bits(b)) // 1266679808

因此浮点数运算是不满足结合率的!

浮点数分类

首先是符号位,根据符号位可以分为正数和负数两类(包括两种0)。不过符号位分类比较简单,这里忽略。

此外,根据指数位和有效数字部分的不同组合,可以分为以下几种:

switch {
case 指数 在 1-254 之间:
    // 规范的浮点数(不是0,也不是很小的浮点数)
    // 也就是小数点前是1,但是这位被省略了
    // 我们平时见到的非0浮点数大多数都是这类
case 指数 == 0:
    if 有效数 == 0 {
        // 这是 0
    } else {
        // 非规范的浮点数,对应很小的浮点数
        // 也就是小数点前是0,但是这位被省略了
    }
case 指数 == 255:
    if 有效数 == 0 {
        // 无穷
    } else {
        // NaN
    }
}

所以说指数部分是IEEE754的精髓。

浮点数在数轴的分布

Go 之父说:不懂浮点数不配当码农

浮点数分布不均匀:越靠近原点越密;同一指数阶码分布均匀。

课后习题

  1. JSON为何没有支持int64类型?

  2. 可参与算术运算的float32数有多少个?,bit模式有多少种?

Go 之父说:不懂浮点数不配当码农


以上所述就是小编给大家介绍的《Go 之父说:不懂浮点数不配当码农》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

标签

标签

Gene Smith / 张军、陈军亮 / 机械工业出版社 / 2012-6 / 59.00元

本书对标记系统这一概念的内涵和外延进行了系统化的、深入浅出的阐述。从什么是标记系统、标记系统有什么价值,到标记系统的架构和与其他分类系统的对比,再到标签的呈现方式和标记系统的实现细节,作者都用通俗易懂的语言进行了阐述,并附有详细的示例和具体的案例研究。本书的每一章都涵盖了标记系统的一个方面,主要内容包括:标记系统的模型、价值、架构,标签的分类、可视化、管理方法,最后介绍标记系统设计方法。本书带领读......一起来看看 《标签》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换