上一篇讨论浮点数的文里提到,浮点数和数学上的实数并不完全等同。但因为四则运算的需要,浮点数的取值依然定义了若干特殊情况。
浮点数有零吗
除以0怎么办
数学上,以0为除数的除法自然没有意义,但计算机设计者无法要求程序在遇到这种除法时就退出甚至关机,所以必然要纳入考虑。
对于被除数非零的情况,如 3.0/0.0
,把0当作一个无穷小算是自然的考虑,这样就可以得出结论,结果是 +∞
正无穷大。虽然数学上的无穷大并不算做一个数,但浮点数标准真的定义了这个值。
如果被除数是0,即 0.0/0.0
,情况就变棘手了。这个结果该是多少呢?无穷大?不对劲。等于1?也不合理,因为这个结果显然该和正常的1有所区分。为此,IEEE-754额外又定义了一类特殊值,叫做「非数」(NaN, Not a Number)。请注意,这里说的是「一类」而非「一个」,因为NaN真的也不是一个固定表示的数。
但对于除0,浮点数也不只是生成无穷大或NaN就完了,大多数CPU还会生成一个叫做「浮点异常」的东西。浮点异常和编程语言里的异常概念并不等同,它要底层得多,规定在CPU的指令集文档里。在后续讨论浮点环境的章节,我们会深入讨论浮点异常。
Python是个例外,Python解释器对浮点数除0也会生成ZeroDivisionError
异常。想在Python里获得无穷大,可以通过math.inf
常量。
浮点数0的表示是惟一的吗
和你想的……不一样,浮点数的0并不惟一!虽然0的指数和尾数全部为0,但还有一个符号位,既可以为0也可以为1. 如果符号位为1,实际上这个值会变成「负零」!
> dumpFloat64(-0.0)
Sign 1
Exponent 00000000000
Mantissa 0000000000000000000000000000000000000000000000000000
可能你的第一反应是,0的符号位没有任何意义。但这是独立符号位的必然结果,定点数采用补码就避免了两个0的情况。我们也的确可能得到不同符号的0:
> -0 - 0
-0
> -0 + 0
0
不同符号的0自然也会导出不同符号的无穷大:
> 1.0 / 0.0
Infinity
> -1.0 / 0.0
-Infinity
好在根据标准,这两个0永远相等,不用再专门调用某个名似iszero
的函数了(尽管某些语言还真有)。
无穷大
什么是无穷大
用一个非0的浮点数除以0,会产生一个叫做无穷大的值。逻辑上,这个无穷大依然还要按照浮点数的结构,表示为一串二进制位。根据浮点数标准,指数全1而尾数全0的数被规定为无穷大。因为还有一个符号位,所以无穷大也分为正无穷大和副无穷大。这倒很合理,1.0/0.0
是正无穷大,-1.0/0.0
是负无穷大嘛。
用第一章的dumpFloat64()
函数打印一个正无穷大的值:
> dumpFloat64(1.0/0.0)
Sign 0
Exponent 11111111111
Mantissa 0000000000000000000000000000000000000000000000000000
如何判断一个数是否是无穷大
因为正负无穷大各自都有惟一表示,因此可以直接用相等去判断一个数是否是无穷大。但出于可读性考虑,以及避免遗漏正负情况,大多数语言都提供了数学函数做这个判断:
- C/C++: 标准库math.h的
isinf(x)
或Clang/GCC扩展的__builtin_isinf(x)
- Java:
Double.isInfinite(x)
和Float.isInfinite(x)
- Python:
math.isinf(x)
- Ruby:
x.infinite?
- JavaScript:
!Number.isNaN(x) && !Number.isFinite(x)
无穷大和其他浮点数的关系是怎样的
和你想的一样:
- 对于既不是无穷大也不是NaN的浮点数,正无穷大比它们都大,负无穷大比它们都小,正无穷大自然也比负无穷大大
- 正无穷大或负无穷大都和自己相等
NaN
为什么会存在NaN这种东西
浮点数是对数学运算的模拟,因此需要有个概念来表达「运算错误」,否则浮点数作为类型的定义就是不完整的。实用地说,除了两个0相除这种情况,sqrt(-1)
或者log(-1)
同样会返回NaN,因为在没有复数的情况下,并没有一个正常数值能够表示这种错误结果。
除此之外,NaN参与运算的结果也会是NaN:
> NaN + 5
NaN
> Math.sqrt(NaN)
NaN
> NaN * 0
NaN
例外是比较操作,单独的NaN和任何数的任何比较都是false,包括它自己:
> NaN > -Infinity
false
> NaN === NaN
false
> NaN > 0
false
在JavaScript里,parseFloat
和parseInt
解析到非法内容,也会返回NaN:
> parseFloat('x')
NaN
> parseInt('?')
NaN
NaN是如何表示的
指数全1而尾数全0的数用以表示无穷大,而指数全1尾数非全0就被用来表示NaN了。
> dumpFloat64(NaN)
Sign 0
Exponent 11111111111
Mantissa 1000000000000000000000000000000000000000000000000000
只要尾数部分不全为0,不管具体是多少都是NaN. 这也意味着NaN的表示并不惟一,我们不能再用等号去判断一个数是否是NaN,更何况,本来NaN和自己比较相等的结果就是false.
然而……尾数不同的NaN可能亦有区别。
如何判断一个数是否是NaN
最简单的方法,当然是和判断无穷类似:调库。
- C/C++:
isnan(x)
或Clang/GCC扩展的__builtin_isnan(x)
- Java:
Double.isNaN(x)
和Float.isNaN(x)
- Python:
math.isnan(x)
- Ruby:
x.nan?
- JavaScript:
Number.isNaN(x)
不过既然我们已经知道了原理,不妨实现一个土法isNaN:
function poorIsNaN(value) {
let buffer = new ArrayBuffer(8)
let float64 = new Float64Array(buffer)
float64[0] = value
let uint8 = new Uint8Array(buffer)
// Do not reverse on Big Endian
let binary = Array.from(uint8).reverse()
return (binary[0] & 127) === 127 &&
(binary[1] & 240) == 240 &&
((binary[1] & 15) | binary[2] | binary[3] |
binary[4] | binary[5] | binary[6] | binary[7]) != 0
}
要注意,这段代码对大端平台并不管用。
NaN的尾数部分有何影响
根据IEEE-754标准,尾数部分第一位为0的NaN被称作Signaling NaN,以1开头的则被称作Quiet NaN. 在大多数硬件架构上,对Signaling NaN的操作会触发浮点异常,而Quiet NaN则不会。
发表回复