在 Rust 中,處理數值運算時,理解底層的資料表示至關重要,尤其是浮點數。本文將深入探討浮點數的內部結構,包括符號位、指數和尾數,並解釋如何使用位元運算來解析這些組成部分。首先,我們會簡要討論整數溢位的問題,然後逐步深入浮點數的表示方式,並提供程式碼範例和圖表,幫助讀者更好地理解。接著,我們將詳細說明如何分離和解碼浮點數的各個組成部分,包括特殊情況的處理,例如次正規數、無窮大和 NAN。最後,我們會探討如何計算尾數的權重,以及如何使用位元遮罩來提取特定位元的值。

Rust 中的型別轉換

Rust 的 std::mem::transmute 函式允許將一個型別的參考轉換為另一個型別的參考,前提是兩個型別的大小必須相同。這種轉換是非常底層的,並不考慮到高層的語義,純粹只是對位元進行重新解釋。

整數的生命週期

在前面的章節中,我們討論了整數的型別,例如 i32u8usize。整數就像小小的魚一樣,在自己的範圍內運作得非常好,但如果超出這個範圍,它們就會迅速死亡。

整數在電腦中佔據固定的位元數,根據型別的不同。與浮點數不同,整數不能犧牲精確度來擴充套件自己的範圍。一旦位元被填滿為 1,唯一的前進道路就是回到所有 0。

讓我們來看看 16 位元的整數,可以表示從 0 到 65,535 之間的數字。如果你想計算到 65,536 怎麼辦?讓我們來看看。

整數溢位

我們正在調查的技術術語是整數溢位。整數溢位的一種最無害的方式是使用遞增運算子。以下的程式碼(ch5/ch5-to-oblivion.rs)是一個簡單的例子:

fn main() {
    let mut i: u16 = 0;
    print!("{}..", i);
    //...
}

在這個例子中,我們定義了一個 16 位元的無符號整數 i,初始值為 0。如果我們對 i 進行遞增運算,當 i 的值達到 65,535 時,下一次遞增運算將會導致整數溢位。

解決整數溢位

那麼,如何解決整數溢位呢?有一種方法是使用更大的整數型別,例如 u32u64。這樣可以提供更大的範圍,以避免整數溢位。

另一種方法是使用模運算,以確保整數在一定的範圍內。例如,如果你想計算一個迴圈計數器,你可以使用模運算來確保計數器在一定的範圍內。

瞭解整數溢位的問題

當我們嘗試執行清單 5.3 的程式時,程式的執行結果並不如預期。讓我們來看看輸出的結果:

$ rustc ch5-to-oblivion.rs &&./ch5-to-oblivion
0..1000..2000..3000..4000..5000..6000..7000..8000..9000..10000..
11000..12000..13000..14000..15000..16000..17000..18000..19000..20000..
21000..22000..23000..24000..25000..26000..27000..28000..29000..30000..
31000..32000..33000..34000..35000..36000..37000..38000..39000..40000..
41000..42000..43000..44000..45000..46000..47000..48000..49000..50000..
51000..52000..53000..54000..55000..56000..57000..58000..59000..60000..
thread 'main' panicked at 'attempt to add with overflow', ch5-to-oblivion.rs:5:7
note: run with `RUST_BACKTRACE=1` environment variable

從輸出結果中,我們可以看到程式嘗試將 i 的值不斷增加 1000,直到發生整數溢位(integer overflow)。這是因為 i 的資料型別是 i32,它只能儲存從 -2^31 到 2^31 - 1 的整數值。

程式碼分析

讓我們來看看程式碼的第 5 行:

i += 1000;

這行程式碼嘗試將 i 的值增加 1000。但是當 i 的值已經超過了 i32 的最大值時,就會發生整數溢位。

解決方案

為瞭解決這個問題,我們可以使用 u64i64 作為 i 的資料型別,它們可以儲存更大的整數值。或者,我們可以使用 usize,它是無符號整數,能夠儲存任意大的正整數值。

let mut i: u64 = 0;
loop {
    i += 1000;
    print!("{}..", i);
    if i % 10000 == 0 {
        print!("\n");
    }
}

或者,我們可以使用 checked_add 方法來檢查是否會發生整數溢位:

let mut i = 0;
loop {
    if let Some(new_i) = i.checked_add(1000) {
        i = new_i;
        print!("{}..", i);
        if i % 10000 == 0 {
            print!("\n");
        }
    } else {
        break;
    }
}

這樣就可以避免整數溢位的問題。

圖表翻譯:

  flowchart TD
    A[開始] --> B[初始化 i]
    B --> C[迴圈增加 i]
    C --> D[檢查整數溢位]
    D -->|是| E[終止迴圈]
    D -->|否| F[繼續迴圈]
    F --> C

在這個流程圖中,我們可以看到程式的執行流程。首先,初始化 i 的值。然後,進入迴圈,增加 i 的值。在每次迴圈中,檢查是否會發生整數溢位。如果會發生,則終止迴圈。否則,繼續迴圈。

瞭解整數溢位和端序

在程式設計中,瞭解整數溢位和端序(endianness)是非常重要的。整數溢位發生在當一個整數超出其最大值或最小值時,端序則是指電腦如何儲存多位元組的整數。

整數溢位

當一個整數超出其最大值或最小值時,就會發生整數溢位。例如,若我們有一個 8 位元組的無符號整數(u8),其最大值為 255。如果我們將 200 和 200 相加,結果將超出 255,導致整數溢位。

let (a, b) = (200, 200);
let c: u8 = a + b; // 會發生整數溢位

在 Rust 中,可以使用 #[allow(arithmetic_overflow)] 屬性來允許整數溢位。但是,這可能會導致程式錯誤或產生不正確的結果。

端序

端序(endianness)是指電腦如何儲存多位元組的整數。有兩種主要的端序:大端序(big-endian)和小端序(little-endian)。

  • 大端序:最重要的位元組放在最前面。例如,32 位元組的整數 0x12345678 將被儲存為 0x12 0x34 0x56 0x78
  • 小端序:最不重要的位元組放在最前面。例如,32 位元組的整數 0x12345678 將被儲存為 0x78 0x56 0x34 0x12

不同的 CPU 架構可能使用不同的端序。例如,x86-64 架構使用小端序,而某些其他架構可能使用大端序。

use std::mem::transmute;

fn main() {
    let big_endian: [u8; 4] = [0xAA, 0xBB, 0xCC, 0xDD];
    let little_endian: [u8; 4] = [0xDD, 0xCC, 0xBB, 0xAA];

    let a: i32 = unsafe { transmute(big_endian) };
    let b: i32 = unsafe { transmute(little_endian) };

    println!("{} vs {}", a, b);
}
圖表翻譯:
  graph LR
    A[整數溢位] -->|發生在|> B[超出最大值或最小值]
    B -->|可能導致|> C[程式錯誤或不正確結果]
    C -->|可以使用|> D[#[allow(arithmetic_overflow)]]
    D -->|允許整數溢位|> E[但需要小心使用]
    
    F[端序] -->|有兩種|> G[大端序和小端序]
    G -->|大端序|> H[最重要的位元組放在最前面]
    G -->|小端序|> I[最不重要的位元組放在最前面]

內容解密:

上述程式碼示範了整數溢位和端序的概念。首先,我們定義了一個無符號整數 c,並將其指定為 a + b。這可能會發生整數溢位,因為 a + b 的結果可能超出 u8 的最大值。

然後,我們使用 transmute 函式將大端序和小端序的位元組陣列轉換為 i32 整數。這個過程需要注意端序,以確保正確的結果。

最後,我們印出 ab 的值,以示範大端序和小端序的差異。

程式碼解說:

  • let (a, b) = (200, 200);:定義兩個變數 ab,並指定為 200。
  • let c: u8 = a + b;:定義一個無符號整數 c,並指定為 a + b。這可能會發生整數溢位。
  • let big_endian: [u8; 4] = [0xAA, 0xBB, 0xCC, 0xDD];:定義一個大端序的位元組陣列。
  • let little_endian: [u8; 4] = [0xDD, 0xCC, 0xBB, 0xAA];:定義一個小端序的位元組陣列。
  • let a: i32 = unsafe { transmute(big_endian) };:使用 transmute 函式將大端序的位元組陣列轉換為 i32 整數。
  • let b: i32 = unsafe { transmute(little_endian) };:使用 transmute 函式將小端序的位元組陣列轉換為 i32 整數。
  • println!("{} vs {}", a, b);:印出 ab 的值,以示範大端序和小端序的差異。

5.2 位元序與位元編碼

在電腦中,資料的儲存和傳輸都需要考慮位元序(endianness)的問題。位元序是指電腦儲存或傳輸多位後設資料的順序,可以是大端序(big-endian)或小端序(little-endian)。大端序是指最重要的位元(MSB)放在最前面,而小端序則是最不重要的位元(LSB)放在最前面。

例如,假設我們要儲存一個 32 位元的整數 0x12345678,大端序和小端序的儲存方式如下:

  • 大端序:0x12 0x34 0x56 0x78
  • 小端序:0x78 0x56 0x34 0x12

在過去,位元序的問題曾經是電腦界的一個大問題,尤其是在伺服器市場。不同的處理器和作業系統有不同的位元序,導致資料交換和傳輸的困難。然而,現在的情況已經改善,Intel 的小端序已經成為事實上的標準。

除了位元序之外,還有一個相關的問題,就是位元編碼(bit numbering)。位元編碼是指電腦如何編碼單個位元的順序。例如,假設我們有一個 u8 整數,代表值 3,該如何編碼?是 0000_0011 還是 1100_0000?電腦的位元編碼偏好決定了這個問題的答案。

5.3 十進位制數字的表示

在本文中,我們將探討如何壓縮十進位制數字的表示。機器學習模型通常需要儲存和分發大量資料,這些資料通常是浮點數。浮點數的表示方式可以壓縮,以節省儲存空間。

首先,我們需要了解浮點數在電腦中的表示方式。浮點數是使用科學記號法表示的,每個浮點數都可以分解為三部分:符號、 mantissa 和 指數。符號決定了數字的正負,mantissa 是一個固定長度的二進位制小數,指數則決定了 mantissa 的尺度。

5.4 浮點數字

浮點數字是使用科學記號法表示的,每個浮點數都可以分解為三部分:符號、 mantissa 和 指數。例如,數字 3.14 可以表示為 1.4142 × 10^0

浮點數字在電腦中的表示方式可以分為兩種:單精確度(float32)和雙精確度(float64)。單精確度浮點數使用 32 位元表示,而雙精確度浮點數使用 64 位元表示。

  graph LR
    A[符號] -->|決定正負|> B[Mantissa]
    B -->|固定長度二進位制小數|> C[指數]
    C -->|決定尺度|> D[浮點數字]

內容解密:

在上面的 Mermaid 圖表中,我們展示了浮點數字的組成部分。符號決定了數字的正負,mantissa 是一個固定長度的二進位制小數,指數則決定了 mantissa 的尺度。這個圖表幫助我們瞭解浮點數字在電腦中的表示方式。

圖表翻譯:

上面的圖表展示了浮點數字的組成部分。符號、mantissa 和指數都是浮點數字的重要組成部分。圖表中,我們使用了 Mermaid 的語法來描述這些部分之間的關係。這個圖表可以幫助我們更好地理解浮點數字在電腦中的表示方式。

浮點數的奧秘

浮點數是一種特殊的數字格式,能夠用固定寬度的位元來表示非常大的或非常小的數值。要了解浮點數的工作原理,我們需要先了解科學記號法(scientific notation)。

科學記號法

科學記號法是一種用於表示非常大或非常小的數值的方法。它由三部分組成:符號(sign)、尾數(mantissa)和基數(radix)。符號用於表示數值的正負,尾數表示數值的大小,基數則是用於表示數值的尺度。

例如,數值 1.898 × 10^27 和 3.801 × 10^-4 都是使用科學記號法表示的。這兩個數值雖然非常大和非常小,但都使用相同的字元數來表示。

浮點數格式

浮點數格式是根據科學記號法的。它由三個欄位組成:符號位(sign bit)、尾數(mantissa)和指數(exponent)。符號位用於表示數值的正負,尾數表示數值的大小,指數則用於表示數值的尺度。

在浮點數格式中,基數(radix)被定義為 2,因此不需要在位元模式中表示。這使得浮點數格式更加緊湊。

f32 格式

f32 格式是一種 32 位元的浮點數格式。它由三個欄位組成:符號位、指數和尾數。符號位佔 1 位元,指數佔 8 位元,尾數佔 23 位元。

例如,數值 42.42 可以被編碼為 f32 格式的位元模式:01000010001010011010111000010100。這可以更加緊湊地表示為 0x4229AE14。

解碼浮點數

要解碼浮點數,我們需要使用以下公式:

n = -1^sign_bit × mantissa × Radix^(exponent - Bias)

其中,sign_bit 是符號位,mantissa 是尾數,exponent 是指數,Radix 是基數,Bias 是偏置值。

在 f32 格式中,Radix 被定義為 2,Bias 被定義為 127。因此,公式可以簡化為:

n = -1^sign_bit × mantissa × 2^(exponent - 127)

這個公式可以用於解碼任何 f32 格式的浮點數。

浮點數的組成與特點

浮點數是一種數字表示法,允許用來表示非常大或非常小的數值。它由三個部分組成:符號位(sign bit)、尾數(mantissa)和基數(radix)。

符號位(Sign Bit)

符號位用來表示浮點數的正負性。當符號位為 0 時,表示浮點數為正;當符號位為 1 時,表示浮點數為負。

尾數(Mantissa)

尾數是浮點數的小數部分。它是一個二進位制小數,表示為 1.xxxxxx,其中 xxxxxx 是尾數的二進製表示。

基數(Radix)

基數是浮點數的基礎,通常為 2(二進位制)。它用來表示浮點數的大小。

浮點數的表示

浮點數可以表示為:

n = (-1)^sign_bit * mantissa * radix^(exponent - bias)

其中,n 是浮點數的值,sign_bit 是符號位,mantissa 是尾數,radix 是基數,exponent 是指數,bias 是偏差值。

例子

假設我們要表示浮點數 42.42。首先,我們需要將其轉換為二進製表示:

42.42 = 1.010101 * 2^5

然後,我們可以將其表示為浮點數:

n = (-1)^0 * 1.010101 * 2^5

n = 1 * 1.010101 * 32

n = 42.42

特點

浮點數有一些特點需要注意:

  • 零的表示:浮點數可以表示為 0 和 -0,儘管它們的值相同,但它們的二進製表示不同。
  • 非數值(NAN)的表示:浮點數可以表示為非數值(NAN),它們的二進製表示相同,但它們的值不同。

表格

以下表格顯示了 42.42 的浮點數表示:

部分二進製表示
符號位0
尾數1.010101
指數5

這個表格顯示了 42.42 的浮點數表示,其中符號位為 0,尾數為 1.010101,指數為 5。

浮點數的組成與編碼

浮點數是一種用於表示非常大或非常小的數值的數字格式。它由三個部分組成:符號位(Sign bit)、指數(Exponent)和尾數(Mantissa)。在32位浮點數(f32)中,這些部分的編碼方式如下:

  • 符號位(s):佔1位,位於最左邊,用於表示數值的正負。
  • 指數(t):佔8位,位於符號位之後,用於表示數值的尺度。
  • 尾數(m):佔23位,位於指數之後,用於表示數值的精確度。

指數編碼

指數的編碼使用的是偏置編碼,即在實際指數值上加上一個偏置值(127),以確保指數始終為正數。這樣可以簡化比較和運算的過程。

浮點數的表示

浮點數可以用以下公式表示:

(-1)^s * 2^(t-127) * 1.m

其中,s是符號位,t是指數,m是尾數。

範例

假設有一個32位浮點數,其二進製表示為:

0 10000100 01010011010111000010100

根據上述格式,可以分解為:

  • 符號位:0(正數)
  • 指數:10000100(132),減去偏置127後為5
  • 尾數:01010011010111000010100(1.325625)

因此,這個浮點數可以表示為:

1.325625 * 2^5 = 42.5

Rust中的浮點數

在Rust程式設計語言中,浮點數的編碼和表示與上述相同。Rust提供了f32和f64兩種浮點數型別,分別對應32位和64位浮點數。

5.4.2 分離符號位

要分離符號位,需要將其他位元向右移動。對於f32浮點數,這涉及向右移動31個位元(» 31)。以下是執行向右移動的程式碼片段。

let n: f32 = 42.42;
let n_bits: u32 = n.to_bits();
let sign_bit = n_bits >> 31;

為了更深入地理解發生了什麼,以下是圖形化的步驟:

  1. 從f32值開始:

let n: f32 = 42.42;

2. 將f32的位元解釋為u32,以允許位元操作:
   ```rust
let n_bits: u32 = n.to_bits();
  1. 將n中的位元向右移動31個位置:

let sign_bit = n_bits » 31;


## 5.4.3 分離指數
要分離指數,需要進行兩個位元操作。首先,執行向右移動以覆寫尾數的位元(>> 23)。然後,使用AND遮罩(& 0xff)來排除符號位。

指數的位元也需要進行解碼步驟。要解碼指數,將其8個位元解釋為帶符號整數,然後從結果中減去127。(如第5.3.2節所討論,127被稱為偏差。)以下列表顯示了描述上兩段中給出的步驟的程式碼。

```rust
let n: f32 = 42.42;
let n_bits: u32 = n.to_bits();
let exponent = (n_bits >> 23) & 0xff;
let decoded_exponent = (exponent as i32) - 127;

需要解決的是符號位的位置。天真地對待它,代表4,294,967,296(2^32)或0,而不是(2^1)或0。

現在,符號位已經被放置在最低有效位置。

浮點數的運算與表示

浮點數在電腦科學中是一種非常重要的資料型別,尤其是在科學計算、工程應用和圖形處理等領域。浮點數的表示和運算對於獲得正確的計算結果至關重要。

浮點數的表示

浮點數通常使用IEEE 754浮點數標準來表示,該標準定義了單精確度(32位)和雙精確度(64位)浮點數的格式。單精確度浮點數由32位元組成,分為三部分:符號位、指數位和尾數位。其中,符號位佔1位,指數位佔8位,尾數位佔23位。

指數的抽取和解碼

要從浮點數中抽取指數,需要進行位元操作。以下是步驟:

  1. 取得浮點數的位元表示:首先,將浮點數轉換為無符號整數(u32),以便進行位元操作。
let n: f32 = 42.42;
let n_bits: u32 = n.to_bits();
  1. 右移指數位:將浮點數的位元向右移23位,以便將指數位移到最右邊。
let exponent_ = n_bits >> 23;
  1. 過濾符號位:使用AND遮罩(& 0xff)來過濾掉符號位,只保留指數位。
let exponent_ = exponent_ & 0xff;
  1. 解碼指數:最後,將指數位轉換為帶符號整數(i32),並減去偏置值(127),即可得到實際的指數值。
let exponent = (exponent_ as i32) - 127;

這些步驟可以有效地從浮點數中抽取和解碼指數,對於深入理解浮點數的運算和表示非常重要。

圖示化過程

以下是上述步驟的圖示化過程:

  flowchart TD
    A[浮點數] --> B[取得位元表示]
    B --> C[右移指數位]
    C --> D[過濾符號位]
    D --> E[解碼指數]
    E --> F[取得實際指數值]

圖表翻譯

上述圖表描述了從浮點數中抽取和解碼指數的步驟。首先,取得浮點數的位元表示,然後右移指數位,過濾掉符號位,最後解碼指數並取得實際指數值。這個過程對於理解浮點數的運算和表示非常重要。

內容解密

上述程式碼描述了從浮點數中抽取和解碼指數的步驟。首先,使用to_bits()方法取得浮點數的位元表示,然後使用右移運算子(»)將指數位移到最右邊。接下來,使用AND遮罩(& 0xff)來過濾掉符號位,只保留指數位。最後,將指數位轉換為帶符號整數(i32),並減去偏置值(127),即可得到實際的指數值。這個過程可以有效地從浮點數中抽取和解碼指數。

解析浮點數的尾數

要解析浮點數的尾數(mantissa),我們需要將其23個位元隔離出來。雖然可以使用AND遮罩(& 0x7fffff)來移除符號位和指數,但是這一步其實是可選的,因為後續的解碼步驟可以直接忽略不相關的位元。不幸的是,尾數的解碼步驟比指數的解碼步驟複雜得多。

尾數解碼步驟

要解碼尾數的位元,需要將每個位元乘以對應的權重。第一個位元的權重是0.5,之後每個位元的權重都是前一個位元權重的一半。例如,0.5(2^(-1))、0.25(2^(-2))等,直到0.00000011920928955078125(2^(-23))。此外,還有一個隱含的第24個位元,代表1.0(2^0),除非觸發特殊情況,否則這個位元始終被視為是開啟的。

特殊情況

特殊情況由指數的位元決定:

  • 當指數的所有位元都是0時,尾數的位元代表次正常數(denormal numbers)。這種變化增加了可以被代表的近零十進位制數字的數量。正式地說,次正常數是一個介於0和正常行為可以代表的最小數字之間的數字。
  • 當指數的所有位元都是1時,十進位制數字是無窮大(∞)、負無窮大(-∞)或不是一個數字(NAN)。NAN值表示特殊情況,其中數值結果在數學上是未定義的(例如0÷0),或是其他無效的情況。

NAN值的運算

涉及NAN值的運算通常會產生反直覺的結果。例如,測試兩個值是否相等始終是假,即使兩個位元模式完全相同。一個有趣的事實是,f32格式約有4.2百萬(~2^22)個位元模式代表NAN。

實作非特殊情況的程式碼

以下程式碼實作了非特殊情況下的浮點數解析:

let n: f32 = 42.42;
let n_bits: u32 = n.to_bits();

這段程式碼首先定義了一個浮點數n,然後使用to_bits()方法將其轉換為32位元的無符號整數n_bits。這樣就可以進一步解析浮點數的各個部分,包括符號位、指數和尾數。

解析浮點數的尾數部分

在浮點數的表示中,尾數(mantissa)是除符號位和指數位之外的其餘部分。要解析浮點數的尾數部分,我們可以按照以下步驟進行。

步驟1:將浮點數轉換為無符號整數

首先,我們需要將浮點數轉換為無符號整數,以便進行位元操作。這可以透過使用 to_bits() 方法實作。

let n: f32 = 42.42;
let n_bits: u32 = n.to_bits();

步驟2:初始化尾數變數

接下來,我們需要初始化一個變數來儲存尾數的值。初始值設定為 1.0。

let mut mantissa: f32 = 1.0;

步驟3:迭代每個位元

然後,我們需要迭代浮點數的每個位元,從最低位元開始(第 0 位)到最高位元(第 23 位)。

for i in 0..23 {
    //...
}

步驟4:檢查每個位元是否為 1

在迭代過程中,我們需要檢查每個位元是否為 1。如果某個位元為 1,則表示該位元對應的權重需要被新增到尾數中。

let mask = 1 << i;
let one_at_bit_i = n_bits & mask;
if one_at_bit_i!= 0 {
    //...
}

步驟5:計算權重並新增到尾數中

如果某個位元為 1,我們需要計算該位元對應的權重,並將其新增到尾數中。權重的計算公式為 2^(-23 + i),其中 i 是位元的位置。

let i_ = i as f32;
let weight = 2_f32.powf(i_ - 23.0);
mantissa += weight;

完整程式碼

以下是完整的程式碼:

let n: f32 = 42.42;
let n_bits: u32 = n.to_bits();
let mut mantissa: f32 = 1.0;

for i in 0..23 {
    let mask = 1 << i;
    let one_at_bit_i = n_bits & mask;
    if one_at_bit_i!= 0 {
        let i_ = i as f32;
        let weight = 2_f32.powf(i_ - 23.0);
        mantissa += weight;
    }
}

內容解密:

上述程式碼的主要目的是解析浮點數的尾數部分。首先,我們將浮點數轉換為無符號整數,以便進行位元操作。然後,我們初始化一個變數來儲存尾數的值。接下來,我們迭代每個位元,檢查每個位元是否為 1。如果某個位元為 1,我們計算該位元對應的權重,並將其新增到尾數中。最終,尾數變數 mantissa 將儲存著浮點數的尾數部分。

圖表翻譯:

  graph LR
    A[浮點數] --> B[轉換為無符號整數]
    B --> C[初始化尾數變數]
    C --> D[迭代每個位元]
    D --> E[檢查每個位元是否為 1]
    E -->|是| F[計算權重並新增到尾數中]
    E -->|否| D
    F --> D

圖表說明:

上述圖表展示了浮點數尾數部分解析的過程。首先,浮點數被轉換為無符號整數。然後,尾數變數被初始化。接下來,每個位元被迭代,檢查是否為 1。如果某個位元為 1,其對應的權重被計算並新增到尾數中。最終,尾數變數儲存著浮點數的尾數部分。

建立浮點數的可變值

首先,我們需要建立一個可變的浮點數(f32)來代表隱含的第24位。這個值初始化為1.0。

let mut mantissa: f32 = 1.0;

迭代小數位元

接下來,我們需要迭代小數位元的每一位,從第0位到第23位。對於每一位,如果該位元為1,我們就將該位元的權重加到mantissa變數中。

for i in 0..23 {
    let mask = 1 << i;
    let one_at_bit_i = n_bits & mask;
    
    if one_at_bit_i!= 0 {
        let i_ = i as f32;
        let weight = 2_f32.powf(i_ - 23.0);
        mantissa += weight;
    }
}

迭代過程解釋

在迭代過程中,我們使用一個暫時變數i來表示目前正在處理的位元位置。然後,我們建立一個位元遮罩(mask),這個遮罩允許第i位元透過。例如,當i等於5時,位元遮罩是0b100000(二進位制),這樣就可以檢查第5位元是否為1。

如果第i位元是1(即one_at_bit_i不等於0),我們計算該位元的權重,並將其加到mantissa中。權重是根據2的負冪計算的,公式為2^(-23 + i),其中i是目前的位元位置。

從底層實作到高階應用的全面檢視顯示,Rust 的型別轉換功能 std::mem::transmute 允許在大小相同的型別之間進行位元層級的重新詮釋,展現了其強大的底層操控能力,但也伴隨著安全風險。深入剖析浮點數的表示方式,我們理解了符號位、指數和尾數如何編碼及解碼,以及特殊情況如次正規數和 NAN 的處理。精確的位元操作和解碼公式是理解浮點數運算的關鍵。然而,尾數解碼的複雜性和特殊情況的處理,也凸顯了浮點數運算的微妙之處。對於追求極致效能的系統程式設計,理解這些底層機制至關重要。玄貓認為,Rust 提供的工具和方法,讓開發者得以在效能和安全性之間取得平衡,但仍需謹慎使用 unsafe 區塊,並深入理解其底層原理,才能避免潛在的錯誤。隨著 Rust 生態系統的持續發展,我們預見其在高效能運算和嵌入式系統等領域的應用將更加廣泛。