小数,也称浮点数, 在计算机中使用比特来表示这种数字。和整数一样,你可以为小数的存储选择不同的宽度的比特,你可以使用f32,也可以使用f64。但是一个有趣的问题是,有些时候你不要使用全部的比特宽度来表示有限的几个浮点数,比如在稀疏矩阵中,很多数字都是1或者-1或者0。好在通常来说小数是可以进行压缩的。
科学计数法
要理解计算机存储的浮点数,你需要先理解科学技术法。形如2.56✖10^23的数字就是使用了科学计数法。
它由四部分构成:
- 符号位,也就是正负。
- 尾数,也就是小数点后面的数字。
- 基数,也就是上面例子中的10。
- 指数,也就是上面例子中的23。
计算机统一使用2作为基数,所以现在只需要考虑符号、尾数、指数。
从比特到小数
尽管计算机使用比特表示小数,但你在日常开发中使用的肯定不是比特。然而你确实可以把十进制小数转化为比特,反之也是可以。
如果把从小数到比特的过程也算上,那么用代码实现这种转化就需要三个过程:
- 把小数代表的比特提取出来,并进行对齐。
- 把比特解码成符号、尾数、指数表示的数字。
- 把上一步的数字组合成十进制小数。
你会注意到,在第一步还进行了对齐操作。因为每一部分的比特并不是紧凑的。尾数的比特和指数的比特需要对应到自己所属的那一部分宽度,不能逾越。
这一步需要使用比特位运算,进行无符号左移、右移以及“与运算”。
#![allow(dead_code)]const BIAS: i32 = 127;const RADIX: f32 = 2.0;fn deconstruct_f32(n: f32) -> (u32, u32, u32) {let n_: u32 = unsafe { std::mem::transmute(n) };let sign = (n_ >> 31) & 1;let exponent = (n_ >> 23) & 0xff;let fraction = 0b00000000_01111111_11111111_11111111 & n_;(sign, exponent, fraction)}fn decode_f32_parts(sign: u32, exponent: u32, fraction: u32) -> (f32, f32, f32) {let signed_1 = (-1.0_f32).powf(sign as f32);let exponent = (exponent as i32) - BIAS;let exponent = RADIX.powf(exponent as f32);let mut mantissa: f32 = 1.0;for i in 0..23_u32 {let one_at_bit_i = 1 << i;if (one_at_bit_i & fraction) != 0 {mantissa += 2_f32.powf(i as f32 - 23.0);}}(signed_1, exponent, mantissa)}fn f32_from_parts(sign: f32, exponent: f32, mantissa: f32) -> f32 {sign * exponent * mantissa}#[cfg(test)]mod test {use super::{decode_f32_parts, deconstruct_f32, f32_from_parts};#[test]fn test_me() {let n: f32 = 69.6902;let (signbit, exponent, fraction) = deconstruct_f32(n);let (sign, exponent, mantissa) = decode_f32_parts(signbit, exponent, fraction);let reconstituted_n = f32_from_parts(sign, exponent, mantissa);println!("{} -> [sign :{}, exponent: {}, mantissa: {:?}] -> {}",n, signbit, exponent, mantissa, reconstituted_n);}}
