Julia 浮点数

                     

贡献者: xzllxls

   本文授权转载自郝林的 《Julia 编程基础》。原文链接:第 5 章 数值与运算

1. 5.3 浮点数

   浮点数可以用来表示小数。在抽象类型 AbstractFloat 之下,有 4 个具体的浮点数类型。它们是 BigFloatFloat16Float32Float64

   我们先说后 3 个类型。

5.3.1 精度与换算

   这 3 种通常的浮点数类型分别对应着 3 种不同精度的浮点数。详见下表。

表1:浮点数类型及其取值
类型名 精度 其值占用的比特数
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 如果用二进制形式来表示就是这样的:

julia> bitstring(Float32(-0.75))
"10111111010000000000000000000000"

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。这就是浮点数与其底层存储之间的换算过程。

   对于 Float16Float64 类型的浮点数来说,公式是一样的。只是它们存储那 3 个部分所占用的比特数都会不同。不过,对于一些特殊的浮点数(如正无穷、负无穷等),这个公式就不适用了。至于怎样换算,我们就不在此讨论了。如果你有兴趣,可以去阅读最新的 IEEE 754 技术标准。

   上面示例中的函数 bitstring 会把一个数值中的所有比特完全平铺开,并把它们原封不动地塞到一个字符串当中。这样的字符串就叫做比特串。

   顺便说一下,如果我们想获取一个浮点数在底层存储上的指数部分,可以调用 exponent 函数。该函数会以返回一个 Int64 类型的值。相关的,significand 函数用于获取一个浮点数在底层存储上的尾数部分,其结果值的类型是 Float64

5.3.2 值的表示

   我们可以使用数学中的标准形式来写入一个浮点数的字面量,例如:

julia> -0.75
-0.75

julia> 2.718281828
2.718281828

julia>

   如果浮点数的整数部分或小数部分只包含 0 的话,我们还可以把这个 0 省略掉:

julia> -.5
-0.5

julia> 1.
1.0

julia>

   另外,我们还可以使用科学计数法(E-notation)来表示浮点数,如:

julia> 1e8
1.0e8

julia> 0.5e-6
5.0e-7

julia> 0.25e-2
0.0025

julia>

   注意,这里的 e 表示的是以 10 为底的幂运算。紧挨在它右边的那个整数就是指数。因此,0.25e-2 就相当于 $0.25\times10^{-2}$。

   Julia 的 REPL 环境在必要的时候也会使用科学计数法回显浮点数:

julia> 0.000025
2.5e-5

julia> 2500000.0
2.5e6

julia>

   对于我们在上面写入的这些浮点数,Julia 都会把它们识别为 Float64 类型的值。如果你想把一个浮点数转换为 Float32 类型的,那么有两种方式。一种方式是,使用该类型对应的构造函数。另一种方式是,把科学计数法中的 e 改为 f。比如:

julia> Float32(0.000025)
2.5f-5

julia> typeof(2.5f-5)
Float32

julia>

   注意,这里的 f 表示的同样是以 10 为底的幂运算。只不过由它参与生成的浮点数一定是 Float32 类型的。

   对于 Float16 类型的浮点数来说,我们使用科学计数法表示的时候会有些特殊。它由 3 个部分组成,即:一个用十六进制形式表示的整数、一个代表了以 2 为底的幂运算的字母 p,以及一个代表指数的整数。示例如下:

julia> 0x1p0
1.0

julia> 0x1p1
2.0

julia> 0x1p3
8.0

julia> 0x1p-2
0.25

julia>

   可以看到,在我们改动代表指数的那个整数时,浮点数是以 20.5 的倍数来改变的。显然,使用这种方式表示的浮点数在精度上会比较低。这主要是由于在 p 左边的只能是整数。

   Float16 的这种特殊性不仅在于表示形式。它的底层实现也是比较特殊的。由于在传统的计算机硬件中并没有半精度浮点数这一概念,所以这种浮点数可能无法在硬件层面直接参与运算。Julia 只能采用软实现的方式来支持 Float16,并且在运算的时候把这类值的类型转换成 Float32

5.3.3 特殊的浮点数

   特殊的浮点数包括正零、负零、正无穷、负无穷,以及 $NaN$。

   正零(positive zero)和负零(negative zero)虽然在数学逻辑上是相同的,但是在底层存储上却是不同的。请看下面的代码:

julia> 0.0 == -0.0 
true

julia> bitstring(0.0)
"0000000000000000000000000000000000000000000000000000000000000000"

julia> bitstring(-0.0)
"1000000000000000000000000000000000000000000000000000000000000000"

julia>

   在默认情况下,0.0-0.0 都是 Float64 类型的值,但在这里并不重要。重要的是,在存储时,它们的指数部分和尾数部分都是 0。这是 IEEE 754 技术标准中针对这两个浮点数的特殊二进制表示法。在这种情况下,如果正负号部分是 0,那么它就代表 0.0,否则就代表 -0.0

   与正零和负零相比,正无穷(positive infinity)、负无穷(negative infinity)和 NaN(Not a Number)就更加特殊了。并且,它们对应于不同的浮点数类型都有着不同的标识符。请看下面这张表。

表2:非常特殊的 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 下的名称为基准的。

   Inf16Inf32Inf 代表的都是正无穷。它们一定都大于所有的有限浮点数。因此,我们像下面这样调用 typemax 函数就可以得到对应类型的正无穷:

julia> typemax(Float16), typemax(Float32), typemax(Float64)
(Inf16, Inf32, Inf)

julia>

   相对应的,-Inf16-Inf32-Inf 都代表负无穷。它们一定都小于所有的有限浮点数。所以:

julia> typemax(Float16), typemax(Float32), typemax(Float64)
(Inf16, Inf32, Inf)

julia>

   而 NaN16NaN32NaN 的含义都是非数(或者说不是数)。因此,一些无效操作的结果值以及无法确切定义的浮点数就都归于它们的名下了。比如:

julia> 0 / 0
NaN

julia> Inf - Inf
NaN

julia> Inf16 - Inf16
NaN16

julia> -Inf - -Inf
NaN

julia> Inf / Inf
NaN

julia> Inf32 / Inf32
NaN32

julia> -Inf / Inf
NaN

julia> 0 * Inf
NaN

julia>

   这些运算规则都遵循了 IEEE 754 技术标准中的描述。所以我们也不用专门记忆。等到真正需要的时候再去查阅相关文档就好了。

5.3.4 BigFloat

   BigFloatBase.MPFR 包中定义的一个类型。MPFR 本身是一个具有正确舍入(rounding)功能的用于多精度浮点计算(multiple-precision floating-point computations)的 C 语言程序库。而 Base.MPFR 包只是对这个库再次封装。

   BigFloat 类型代表着任意精度的浮点数。示例如下:

julia> BigFloat(-0.75^68) / 3
-1.06425244334102499005657170926276063
  5124796711655411248405774434407552083333339e-09

julia> typeof(ans)
BigFloat

julia>

   与 BigInt 一样,我们使用以 big 为前缀的非常规字符串也可以构造出 BigFloat 的值,比如:

julia> big"-0.75"
-0.75

julia> typeof(ans)
BigFloat

julia> big"2.718281828"
2.7182818280000000000000000000000000000000000000000000000
  00000000000000000000015

julia> typeof(ans)
BigFloat

julia>

   另外,我们都知道,通常的浮点数类型都有着固定的精度。而且,在默认情况下,Julia 对浮点数的舍入模式是四舍五入(由于计算机无法精确地表示所有小数,而且浮点数的位数有限,所以舍入必然存在,舍入模式也是必须要有的)。然而,对于 BigFloat 类型,我们可以自己设定它的精度和舍入模式。

   通过调用 setprecisionsetrounding 函数,我们可以更改 BigFloat 类型值在参与运算时的默认精度和舍入模式。但要注意,这种更改是全局的!也就是说,更改一旦发生,它就会影响到当前 Julia 程序中所有相关的后续操作。不过,我们可以利用 do 代码块,让这种更改只在当前的代码块中有效。下面是一些示例:

julia> BigFloat(1.01) + parse(BigFloat, "0.2")
1.2100000000000000088817841970012523233890533447265625000
  00000000000000000000007

julia> setrounding(BigFloat, RoundDown)
MPFRRoundDown::MPFRRoundingMode = 3

julia> BigFloat(1.01) + parse(BigFloat, "0.2")
1.2100000000000000088817841970012523233890533447265624999
  9999999999999999999999

julia> setprecision(35) do 
           BigFloat(1.01) + parse(BigFloat, "0.2") 
       end
1.2099999999

julia> BigFloat(1.01) + parse(BigFloat, "0.2") 
1.2100000000000000088817841970012523233890533447265624999
  9999999999999999999999

julia>

   示例中的函数 parse 可以帮助我们把一个字符串值转换成某个数值类型的值。不过,转换是否能够成功就要看字符串的具体内容了。如果不能成功转换,这个函数就会报错。

   至于都有哪些舍入模式,我们可以参看 Base.Rounding.RoundingMode 类型的文档。我们在前面说的默认舍入模式是由常量 Base.Rounding.RoundNearest 代表的。另外,我们在后面讲流程控制的时候还会对 do 代码块进行说明。

                     

© 小时科技 保留一切权利