5.3 浮点数
浮点数可以用来表示小数。在抽象类型AbstractFloat之下,有 4 个具体的浮点数类型。它们是BigFloat、Float16、Float32和Float64。
我们先说后 3 个类型。
5.3.1 精度与换算
这 3 种通常的浮点数类型分别对应着 3 种不同精度的浮点数。详见下表。
表 5-2 浮点数类型及其取值
| 类型名 | 精度 | 其值占用的比特数 |
|---|---|---|
| Float16 | 半(half) | 16 |
| Float32 | 单(single) | 32 |
| Float64 | 双(double) | 64 |
对于这 3 种精度的浮点数,最新的 IEEE 754 技术标准中都有所描述。简单来说,一个浮点数在存储时会由 3 个部分组成,即:正负号部分(sign,简称S)、指数部分(exponent,简称E)和尾数部分(trailing significand,简称T)。例如,一个Float32类型的值会占用 32 个比特,其中的正负号会使用 1 个比特,指数部分会使用 8 个比特,而尾数部分会用掉剩下的 23 个比特。
在通常情况下,这 3 个部分会依照下面的公式来共同表示一个浮点数:
-1^S x 2^E-bias x (1 + 2^1-p x T)
这里的bias指的是偏移量,它会是指数部分的比特序列所能表示的最大正整数。注意,指数部分本身也是有符号的。而p代表的则是正负号部分和尾数部分共占用的比特数。
下面举一个例子。Float32类型的浮点数-0.75如果用二进制形式来表示就是这样的:
1. julia> bitstring(Float32(-0.75))
2. "10111111010000000000000000000000"
4. julia>
在 REPL 环境回显的这个比特串中,最左边的那个1就代表了S。紧挨在S右边的 8 个比特是01111110,转换成十进制数就是126,这就是E。而在E右边的 23 个比特则代表T,即十进制数4194304。另外,对于Float32类型来说,bias就是127,而p则是24。把这些都代入前面的公式就可以得到:
-1^1 x 2^-1 x (1 + 0.5)
最终得出-0.75。这就是浮点数与其底层存储之间的换算过程。
对于Float16或Float64类型的浮点数来说,公式是一样的。只是它们存储那 3 个部分所占用的比特数都会不同。不过,对于一些特殊的浮点数(如正无穷、负无穷等),这个公式就不适用了。至于怎样换算,我们就不在此讨论了。如果你有兴趣,可以去阅读最新的 IEEE 754 技术标准。
上面示例中的函数bitstring会把一个数值中的所有比特完全平铺开,并把它们原封不动地塞到一个字符串当中。这样的字符串就叫做比特串。
顺便说一下,如果我们想获取一个浮点数在底层存储上的指数部分,可以调用exponent函数。该函数会以返回一个Int64类型的值。相关的,significand函数用于获取一个浮点数在底层存储上的尾数部分,其结果值的类型是Float64。
5.3.2 值的表示
我们可以使用数学中的标准形式来写入一个浮点数的字面量,例如:
1. julia> -0.75
2. -0.75
4. julia> 2.718281828
5. 2.718281828
7. julia>
如果浮点数的整数部分或小数部分只包含0的话,我们还可以把这个0省略掉:
1. julia> -.5
2. -0.5
4. julia> 1.
5. 1.0
7. julia>
另外,我们还可以使用科学计数法(E-notation)来表示浮点数,如:
1. julia> 1e8
2. 1.0e8
4. julia> 0.5e-6
5. 5.0e-7
7. julia> 0.25e-2
8. 0.0025
10. julia>
注意,这里的e表示的是以10为底的幂运算。紧挨在它右边的那个整数就是指数。因此,0.25e-2就相当于0.25 * 10^-2。
Julia 的 REPL 环境在必要的时候也会使用科学计数法回显浮点数:
1. julia> 0.000025
2. 2.5e-5
4. julia> 2500000.0
5. 2.5e6
7. julia>
对于我们在上面写入的这些浮点数,Julia 都会把它们识别为Float64类型的值。如果你想把一个浮点数转换为Float32类型的,那么有两种方式。一种方式是,使用该类型对应的构造函数。另一种方式是,把科学计数法中的e改为f。比如:
1. julia> Float32(0.000025)
2. 2.5f-5
4. julia> typeof(2.5f-5)
5. Float32
7. julia>
注意,这里的f表示的同样是以10为底的幂运算。只不过由它参与生成的浮点数一定是Float32类型的。
对于Float16类型的浮点数来说,我们使用科学计数法表示的时候会有些特殊。它由 3 个部分组成,即:一个用十六进制形式表示的整数、一个代表了以2为底的幂运算的字母p,以及一个代表指数的整数。示例如下:
1. julia> 0x1p0
2. 1.0
4. julia> 0x1p1
5. 2.0
7. julia> 0x1p3
8. 8.0
10. julia> 0x1p-2
11. 0.25
13. julia>
可以看到,在我们改动代表指数的那个整数时,浮点数是以2或0.5的倍数来改变的。显然,使用这种方式表示的浮点数在精度上会比较低。这主要是由于在p左边的只能是整数。
Float16的这种特殊性不仅在于表示形式。它的底层实现也是比较特殊的。由于在传统的计算机硬件中并没有半精度浮点数这一概念,所以这种浮点数可能无法在硬件层面直接参与运算。Julia 只能采用软实现的方式来支持Float16,并且在运算的时候把这类值的类型转换成Float32。
5.3.3 特殊的浮点数
特殊的浮点数包括正零、负零、正无穷、负无穷,以及 NaN。
正零(positive zero)和负零(negative zero)虽然在数学逻辑上是相同的,但是在底层存储上却是不同的。请看下面的代码:
1. julia> 0.0 == -0.0
2. true
4. julia> bitstring(0.0)
5. "0000000000000000000000000000000000000000000000000000000000000000"
7. julia> bitstring(-0.0)
8. "1000000000000000000000000000000000000000000000000000000000000000"
10. julia>
在默认情况下,0.0和-0.0都是Float64类型的值,但在这里并不重要。重要的是,在存储时,它们的指数部分和尾数部分都是0。这是 IEEE 754 技术标准中针对这两个浮点数的特殊二进制表示法。在这种情况下,如果正负号部分是0,那么它就代表0.0,否则就代表-0.0。
与正零和负零相比,正无穷(positive infinity)、负无穷(negative infinity)和 NaN(Not a Number) 就更加特殊了。并且,它们对应于不同的浮点数类型都有着不同的标识符。请看下面这张表。
表 5-3 非常特殊的 3 种浮点数
| Float16 | Float32 | Float64 | 含义 | 说明 |
|---|---|---|---|---|
| Inf16 | Inf32 | Inf | 正无穷(positive infinity),统称 Inf | 大于所有有限浮点数的值 |
| -Inf16 | -Inf32 | -Inf | 负无穷(negative infinity),统称 -Inf | 小于所有有限浮点数的值 |
| NaN16 | NaN32 | NaN | 非数(not a number),统称 NaN | 不等于任何浮点数(包括它本身)的值 |
Julia 为这 3 种非常特殊的浮点数一共定义了 9 个常量。它们的名称分别在此表格最左侧的 9 个单元格中。由于浮点数字面量默认都是Float64类型的,所以这些常量的名称也是以Float64下的名称为基准的。
Inf16、Inf32和Inf代表的都是正无穷。它们一定都大于所有的有限浮点数。因此,我们像下面这样调用typemax函数就可以得到对应类型的正无穷:
1. julia> typemax(Float16), typemax(Float32), typemax(Float64)
2. (Inf16, Inf32, Inf)
4. julia>
相对应的,-Inf16、-Inf32和-Inf都代表负无穷。它们一定都小于所有的有限浮点数。所以:
1. julia> typemin(Float16), typemin(Float32), typemin(Float64)
2. (-Inf16, -Inf32, -Inf)
4. julia>
而NaN16、NaN32和NaN的含义都是非数(或者说不是数)。因此,一些无效操作的结果值以及无法确切定义的浮点数就都归于它们的名下了。比如:
1. julia> 0 / 0
2. NaN
4. julia> Inf - Inf
5. NaN
7. julia> Inf16 - Inf16
8. NaN16
10. julia> -Inf - -Inf
11. NaN
13. julia> Inf / Inf
14. NaN
16. julia> Inf32 / Inf32
17. NaN32
19. julia> -Inf / Inf
20. NaN
22. julia> 0 * Inf
23. NaN
25. julia>
这些运算规则都遵循了 IEEE 754 技术标准中的描述。所以我们也不用专门记忆。等到真正需要的时候再去查阅相关文档就好了。
5.3.4 BigFloat
BigFloat是Base.MPFR包中定义的一个类型。MPFR 本身是一个具有正确舍入(rounding)功能的用于多精度浮点计算(multiple-precision floating-point computations)的 C 语言程序库。而Base.MPFR包只是对这个库再次封装。
BigFloat类型代表着任意精度的浮点数。示例如下:
1. julia> BigFloat(-0.75^68) / 3
2. -1.064252443341024990056571709262760635124796711655411248405774434407552083333339e-09
4. julia> typeof(ans)
5. BigFloat
7. julia>
与BigInt一样,我们使用以big为前缀的非常规字符串也可以构造出BigFloat的值,比如:
1. julia> big"-0.75"
2. -0.75
4. julia> typeof(ans)
5. BigFloat
7. julia> big"2.718281828"
8. 2.718281828000000000000000000000000000000000000000000000000000000000000000000015
10. julia> typeof(ans)
11. BigFloat
13. julia>
另外,我们都知道,通常的浮点数类型都有着固定的精度。而且,在默认情况下,Julia 对浮点数的舍入模式是四舍五入(由于计算机无法精确地表示所有小数,而且浮点数的位数有限,所以舍入必然存在,舍入模式也是必须要有的)。然而,对于BigFloat类型,我们可以自己设定它的精度和舍入模式。
通过调用setprecision和setrounding函数,我们可以更改BigFloat类型值在参与运算时的默认精度和舍入模式。但要注意,这种更改是全局的!也就是说,更改一旦发生,它就会影响到当前 Julia 程序中所有相关的后续操作。不过,我们可以利用do代码块,让这种更改只在当前的代码块中有效。下面是一些示例:
1. julia> BigFloat(1.01) + parse(BigFloat, "0.2")
2. 1.210000000000000008881784197001252323389053344726562500000000000000000000000007
4. julia> setrounding(BigFloat, RoundDown)
5. MPFRRoundDown::MPFRRoundingMode = 3
7. julia> BigFloat(1.01) + parse(BigFloat, "0.2")
8. 1.21000000000000000888178419700125232338905334472656249999999999999999999999999
10. julia> setprecision(35) do
11. BigFloat(1.01) + parse(BigFloat, "0.2")
12. end
13. 1.2099999999
15. julia> BigFloat(1.01) + parse(BigFloat, "0.2")
16. 1.21000000000000000888178419700125232338905334472656249999999999999999999999999
18. julia>
示例中的函数parse可以帮助我们把一个字符串值转换成某个数值类型的值。不过,转换是否能够成功就要看字符串的具体内容了。如果不能成功转换,这个函数就会报错。
至于都有哪些舍入模式,我们可以参看Base.Rounding.RoundingMode类型的文档。我们在前面说的默认舍入模式是由常量Base.Rounding.RoundNearest代表的。另外,我们在后面讲流程控制的时候还会对do代码块进行说明。
