在 Rust 中,浮點數的處理涉及到許多底層的位元操作。理解這些操作對於高效的數值計算至關重要。本文將深入探討如何解析和重組浮點數,以及如何利用位元運算來操作浮點數的各個組成部分,包含符號位、指數和尾數。此外,我們也將探討固定點數 Q7 格式,這在資源受限的系統中是一種更為節省空間的數值表示方法。透過位元層級的解析,我們可以更精確地控制浮點數的表示和運算,並針對特定硬體平臺進行最佳化。

圖表翻譯

以下是迭代過程的Mermaid流程圖:

  flowchart TD
    A[開始] --> B[迭代小數位元]
    B --> C{第i位元是否為1}
    C -->|是| D[計算權重並加到mantissa]
    C -->|否| E[繼續下一位元]
    D --> E
    E --> F{是否完成所有位元}
    F -->|是| G[結束]
    F -->|否| B

這個流程圖展示了迭代過程中每一步的邏輯,包括檢查每一位元是否為1、計算權重、加到mantissa中,以及繼續下一位元的處理。

浮點數的深度剖析

浮點數是一種容器格式,包含三個欄位:符號位、指數位和尾數位。要將浮點數轉換為普通數字,需要進行三個任務:提取欄位、解碼欄位和進行算術運算。

提取欄位

首先,需要從浮點數中提取欄位。這可以透過位運算來實作。例如,給定一個 f32 數字,可以使用以下程式碼提取其欄位:

let mask = 1 << i;
let one_at_bit_i = n_bits & mask;

這裡,mask 是一個位遮罩,用於過濾 n_bits 中的位元。當 n_bits 中的位元在位置 i 非零時,one_at_bit_i 將被指定為非零值。

解碼欄位

接下來,需要將提取的欄位解碼為其實際值。例如,給定一個 f32 數字,可以使用以下程式碼解碼其指數位和尾數位:

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

這裡,i_ 是指數位的值,weight 是指數位的權重,mantissa 是尾數位的值。

進行算術運算

最後,需要進行算術運算將浮點數從科學記號轉換為普通數字。例如,給定一個 f32 數字,可以使用以下程式碼進行算術運算:

let result = from_parts(sign, exponent, mantissa);

這裡,sign 是符號位的值,exponent 是指數位的值,mantissa 是尾數位的值。

示例程式碼

以下是完整的示例程式碼:

fn to_parts(n: f32) -> (u32, u32, u32) {
    let bits = n.to_bits();
    let sign = (bits >> 31) & 1;
    let exponent = (bits >> 23) & 0xff;
    let mantissa = bits & 0x7fffff;
    (sign, exponent, mantissa)
}

fn decode(exponent: u32, mantissa: u32) -> (f32, f32) {
    let bias = 127;
    let exponent = (exponent as i32) - bias;
    let mantissa = mantissa as f32 / (1 << 23);
    (exponent, mantissa)
}

fn from_parts(sign: u32, exponent: f32, mantissa: f32) -> f32 {
    let result = (-1.0_f32).powf(sign as f32) * (2.0_f32).powf(exponent) * (1.0_f32 + mantissa);
    result
}

fn main() {
    let n = 42.42_f32;
    let (sign, exponent, mantissa) = to_parts(n);
    let (exponent, mantissa) = decode(exponent, mantissa);
    let result = from_parts(sign, exponent, mantissa);
    println!("Result: {}", result);
}

這個程式碼首先將浮點數 n 分解為其欄位,然後解碼欄位,最後進行算術運算將浮點數轉換為普通數字。

浮點數的位元層級解析與重組

浮點數在電腦中是一種複雜的資料型別,需要透過特定的位元層級解析和重組來正確地表示和運算。下表展示了一個浮點數的位元層級結構:

欄位位元表示實數值
符號位01
指數1000010032
小數部分010100110101110000101001.325625

解析浮點數

在 Rust 中,可以使用位元操作技術來解析浮點數的各個欄位。以下是 deconstruct_f32() 函式的實作:

fn deconstruct_f32(n: f32) -> (u32, u32, u32) {
    let bits: u32 = n.to_bits();
    let sign = (bits >> 31) & 1;
    let exponent = (bits >> 23) & 0xff;
    let mantissa = bits & 0x7fffff;
    (sign, exponent, mantissa)
}

這個函式將浮點數 n 轉換為位元表示,然後使用位元操作來提取符號位、指數和小數部分。

重組浮點數

一旦我們有了浮點數的各個欄位,就可以使用 f32_from_parts() 函式來重組它:

fn f32_from_parts(sign: u32, exponent: u32, mantissa: u32) -> f32 {
    let bits = (sign << 31) | (exponent << 23) | mantissa;
    f32::from_bits(bits)
}

這個函式將符號位、指數和小數部分組合起來,形成一個完整的浮點數位元表示,然後使用 f32::from_bits() 函式將其轉換為浮點數。

測試

我們可以使用以下程式碼來測試這些函式:

fn main() {
    let n: f32 = 42.42;
    let (sign, exp, frac) = deconstruct_f32(n);
    let (sign_, exp_, mant) = decode(sign, exp, frac);
    let n_ = f32_from_parts(sign_, exp_, mant);
    println!("{} -> {}", n, n_);
}

這個程式碼會輸出 42.42 -> 42.42,證明我們的解析和重組函式是正確的。

Rust 的浮點數字面量

Rust 的浮點數字面量有一些特殊的規則。例如,負號有較低的優先順序於方法呼叫,這意味著 -1.0_f32.powf(0.0) 會被解釋為 -(1.0_f32.powf(0.0))。為了避免這種問題,我們可以使用括號來明確指出意圖,例如 (-1.0_f32).powf(0.0)

浮點數的組成與解析

浮點數在電腦中是以二進製表示的,通常使用32位或64位來儲存。瞭解浮點數的組成對於進行數值計算和最佳化是非常重要的。在這篇文章中,我們將探討浮點數的基本結構和如何將其拆分為不同的部分。

浮點數的結構

一個32位的浮點數可以被分為三個部分:符號位(sign)、指數(exponent)和尾數(mantissa)。符號位用於表示數字的正負,指數則用於表示數字的大小,而尾數則包含了數字的精確值。

解析浮點數

要解析一個浮點數,首先需要將其轉換為二進位制格式。然後,根據浮點數的結構,可以提取出符號位、指數和尾數。

fn to_parts(n: f32) -> (u32, u32, u32) {
    let bits = n.to_bits();
    
    // 提取符號位
    let sign = (bits >> 31) & 1;
    
    // 提取指數
    let exponent = (bits >> 23) & 0xff;
    
    // 提取尾數
    let fraction = bits & 0x7fffff;
    
    (sign, exponent, fraction)
}

顯示浮點數的組成部分

有了上述函式後,我們可以輕鬆地顯示出浮點數的各個部分。

fn main() {
    let num: f32 = 3.14159265359;
    let (sign, exponent, fraction) = to_parts(num);
    
    println!("符號位 | {:01b} | {}", sign, if sign == 1 { "-" } else { "+" });
    println!("指數 | {:08b} | {}", exponent, exponent);
    println!("尾數 | {:023b} | {}", fraction, fraction);
}

這樣就完成了浮點數的解析和顯示。透過這個過程,我們可以更好地理解浮點數在電腦中的表示方式和運算原理。

圖表翻譯:

  flowchart TD
    A[浮點數] --> B[符號位]
    A --> C[指數]
    A --> D[尾數]
    B --> E[正負判斷]
    C --> F[大小表示]
    D --> G[精確值計算]

這個流程圖描述了浮點數的組成和解析過程,從而幫助我們更好地理解浮點數在電腦中的運作機制。

浮點數解碼函式

函式定義

fn decode(sign: u32, exponent: u32, fraction: u32) -> (f32, f32, f32) {
    //...
}

解碼過程

在這個函式中,我們首先計算有符號資料的符號位。然後,我們計算指數部分,並將其轉換為實際的指數值。接下來,我們計算小數部分(分數)。

符號位計算

let signed_1 = (-1.0_f32).powf(sign as f32);

這行程式碼計算符號位,根據輸入的 sign 值決定結果是 1 還是 -1。

指數計算

let exponent = (exponent as i32) - BIAS;
let exponent = RADIX.powf(exponent as f32);

這兩行程式碼計算指數部分。首先,我們從輸入的 exponent 值中減去偏移值 BIAS,然後將結果轉換為實際的指數值。

小數部分計算

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

這段程式碼計算小數部分。它遍歷 fraction 的每一位,如果該位為 1,則將對應的權重新增到 mantissa 中。

最終結果

函式傳回三個 f32 值,分別代表符號位、指數和小數部分。

圖表翻譯

  flowchart TD
    A[輸入] --> B[符號位計算]
    B --> C[指數計算]
    C --> D[小數部分計算]
    D --> E[傳回結果]

這個流程圖描述了浮點數解碼函式的執行過程。首先,輸入符號位、指數和小數部分。然後,分別計算符號位、指數和小數部分。最後,傳回結果。

解構浮點數值

浮點數值可以被分解成其組成部分,包括符號、指數和尾數。這個過程對於理解浮點數的運作方式和實作浮點數運算至關重要。

從浮點數中提取符號

要從浮點數中提取符號,我們可以使用位元操作。浮點數的符號位位於最高位,因此我們可以使用邏輯AND操作來提取這一位。

fn extract_sign(float: f32) -> f32 {
    // 使用邏輯AND操作來提取符號位
    let sign_bit = (float as i32) >> 31;
    // 將符號位轉換為f32型別
    sign_bit as f32
}

從浮點數中提取指數

提取指數的過程涉及移除符號位和尾數位。指數位於符號位之後的8位,因此我們需要移除最高位的符號位和最低23位的尾數位。

fn extract_exponent(float: f32) -> f32 {
    // 移除符號位和尾數位
    let exponent = ((float as i32) >> 23) & 0xFF;
    // 將指數轉換為f32型別
    exponent as f32
}

從浮點數中提取尾數

尾數是浮點數中除符號位和指數位之外的所有位。要提取尾數,我們可以使用邏輯AND操作來過濾掉最高8位(符號位和指數位)。

fn extract_mantissa(float: f32) -> f32 {
    // 使用邏輯AND操作來過濾掉最高8位
    let mantissa = float as i32 & 0x007FFFFF;
    // 將尾數轉換為f32型別
    mantissa as f32
}

從部分構建浮點數

構建浮點數的過程涉及組合符號、指數和尾數。這個過程需要將每個部分轉換為其正確的位置並組合起來。

fn from_parts(sign: f32, exponent: f32, mantissa: f32) -> f32 {
    // 將符號、指數和尾數轉換為其正確的位置
    let sign_bit = (sign as i32) << 31;
    let exponent_bits = (exponent as i32) << 23;
    let mantissa_bits = mantissa as i32;
    
    // 組合每個部分
    let float = sign_bit | exponent_bits | mantissa_bits;
    
    // 將結果轉換為f32型別
    float as f32
}

圖表翻譯:

  graph LR
    A[浮點數] -->|提取符號|> B[符號]
    A -->|提取指數|> C[指數]
    A -->|提取尾數|> D[尾數]
    B -->|組合|> E[浮點數]
    C -->|組合|> E
    D -->|組合|> E

內容解密:

上述程式碼示範瞭如何從浮點數中提取符號、指數和尾數,以及如何從這些部分構建浮點數。這個過程對於理解浮點數的運作方式和實作浮點數運算至關重要。每個部分都需要被轉換為其正確的位置並組合起來,以構建一個完整的浮點數。

浮點數解碼過程

浮點數的解碼過程涉及多個步驟,包括符號位、尾數(mantissa)和指數(exponent)的處理。

符號位解碼

符號位是浮點數中的一個位元,用於表示數值的正負。解碼過程中,符號位被轉換為1.0或-1.0,代表正數或負數。這個過程可以使用簡單的運算實作,例如:

let sign = if sign_bit { -1.0 } else { 1.0 };

注意,在某些情況下,可能需要使用括號來明確運算順序,例如:

let sign = if sign_bit { -1.0_f32 } else { 1.0_f32 };

尾數解碼

尾數是浮點數中的一部分,代表數值的小數部分。解碼過程中,尾數被轉換為一個小數值。這個過程可以使用位元操作和算術運算實作,例如:

let mantissa = (raw_mantissa as f32) / (2.0_f32.powi(23));

指數解碼

指數是浮點數中的一部分,代表數值的尺度。解碼過程中,指數被轉換為一個整數值。這個過程可以使用位元操作和算術運算實作,例如:

let exponent = ((raw_exponent as i32) - BIAS) as f32;

注意,在某些情況下,可能需要使用型別轉換來確保正確的運算結果,例如:

let exponent = ((raw_exponent as i32) - BIAS) as f32;

圖表翻譯:

  graph LR
    A[原始浮點數] -->|符號位解碼|> B[符號位]
    A -->|尾數解碼|> C[尾數]
    A -->|指數解碼|> D[指數]
    B -->|計算|> E[最終結果]
    C -->|計算|> E
    D -->|計算|> E

內容解密:

上述過程中,符號位、尾數和指數的解碼都是浮點數解碼的重要步驟。透過這些步驟,可以將原始浮點數轉換為一個易於使用和操作的格式。注意,在實際實作中,可能需要考慮到各種邊界情況和特殊情況,以確保正確的結果。

固定點數字格式

除了使用浮點數格式來表示小數數字外,固定點數字格式也是可用的選擇。這種格式對於表示分數非常有用,並且可以在沒有浮點數運算單元(FPU)的CPU上進行計算,例如微控制器。與浮點數不同,固定點數字格式的小數點位置是固定的,不會根據不同的範圍動態調整。在本文中,我們將使用固定點數字格式來緊湊地表示-1到1之間的值。雖然這種格式會損失一些精確度,但它可以節省大量的空間。

Q格式

Q格式是一種固定點數字格式,使用單個byte來表示數字。這種格式由玄貓建立。具體來說,我們將實作的Q格式版本稱為Q7,表示有7個bit用於表示數字,加上1個符號bit。我們將使用Rust語言來定義這種格式,讓編譯器幫助我們跟蹤數字的符號。同時,我們也可以免費獲得PartialEq和Eq等特徵,從而提供比較運算子。

以下程式碼片段是Q7格式的定義,來自ch5/ch5-q/src/lib.rs檔案。

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Q7(i8);

這種結構體被稱為元組結構體,因為它使用了未命名的欄位。這種結構體提供了一種簡潔的表示法,當欄位不需要直接存取時非常有用。雖然在列表5.11中沒有顯示,但元組結構體也可以包含多個欄位。

Q7格式的優點

使用Q7格式可以帶來以下優點:

  • 緊湊地表示-1到1之間的值
  • 節省空間
  • 可以在沒有FPU的CPU上進行計算

實作Q7格式

要實作Q7格式,我們需要定義一個結構體,並使用Rust語言的derive特性來自動實作某些特徵。以下是實作Q7格式的步驟:

  1. 定義Q7結構體: pub struct Q7(i8);
  2. 使用derive特性自動實作特徵: #[derive(Debug, Clone, Copy, PartialEq, Eq)]

透過這些步驟,我們可以實作Q7格式,並使用它來緊湊地表示-1到1之間的值。

內容解密:

上述程式碼片段中,我們定義了一個名為Q7的結構體,該結構體包含一個i8型別的欄位。這個欄位用於表示-1到1之間的值。透過使用Rust語言的derive特性,我們可以自動實作某些特徵,例如Debug、Clone、Copy、PartialEq和Eq等。這些特徵可以幫助我們跟蹤數字的符號,並提供比較運算子。

圖表翻譯:

以下是Q7格式的Mermaid圖表:

  classDiagram
    Q7 <|-- i8
    class Q7 {
        -i8 value
    }

這個圖表顯示了Q7結構體和i8型別之間的關係。Q7結構體包含一個i8型別的欄位,用於表示-1到1之間的值。

圖表解釋:

上述圖表顯示了Q7格式的結構體和i8型別之間的關係。透過這個圖表,我們可以清楚地看到Q7結構體如何使用i8型別來表示-1到1之間的值。同時,這個圖表也顯示了Rust語言的derive特性如何自動實作某些特徵,例如Debug、Clone、Copy、PartialEq和Eq等。這些特徵可以幫助我們跟蹤數字的符號,並提供比較運算子。

固定點數字格式

固定點數字格式是一種數字表示方法,常用於嵌入式系統或其他資源有限的環境。下面我們將探討固定點數字格式的特點和實作。

Debug模式

Debug模式是用於除錯的模式,允許我們將固定點數字轉換為字串。這對於除錯和logging非常有用。

Clone模式

Clone模式允許我們複製固定點數字。這可以透過實作Clone trait來實作。

Copy模式

Copy模式允許我們進行廉價和隱式的複製。這可以透過實作Copy trait來實作。

PartialEq模式

PartialEq模式允許我們比較固定點數字是否相等。這可以透過實作PartialEq trait來實作。

Eq模式

Eq模式表示所有可能的固定點數字都可以與其他任何可能的固定點數字進行比較。這可以透過實作Eq trait來實作。

從效能最佳化和程式碼精簡的角度來看,使用固定點數字格式(例如 Q7 格式)在特定場景下,相較於浮點數運算,展現出顯著的優勢。分析 Q7 格式的底層實作,可以發現其核心優勢在於以整數運算取代浮點數運算,大幅降低了 CPU 的負擔,尤其在缺乏 FPU 的微控制器等資源受限的裝置上,效能提升尤為明顯。然而,固定點數字格式的精確度受限於整數位元數,在需要高精確度計算的場景下,仍需謹慎評估其適用性。技術限制主要在於數值範圍和精確度損失,需要開發者根據實際應用場景選擇合適的 Q 格式(例如 Q7、Q15 等)。對於注重效能且精確度要求不高的嵌入式系統開發,採用固定點數字格式,特別是 Q 格式,能有效降低系統資源消耗,提升程式碼執行效率。玄貓認為,隨著物聯網裝置的普及和對低功耗運算的需求日益增長,固定點數字格式的應用場景將持續擴大,未來可能出現更多針對特定應用的客製化 Q 格式,以在效能和精確度之間取得更佳平衡。