Rust程式語言可以使用「static」關鍵字來宣告出靜態變數,也可以使用「const」關鍵字來宣告出常數。如果靜態變數並沒有再使用「mut」關鍵字來修飾的話,該靜態變數就是「不可變」的,那麼「不可變的靜態變數」和同樣是不可變的「常數」,會有效能上的差異嗎?



我們先來看看以下程式碼:

const BA_C: [u64; 1000000] = [0; 1000000];
static BA_S: [u64; 1000000] = [0; 1000000];

use std::thread;

fn main() {
    let child = thread::Builder::new()
        .stack_size(8 * 1024 * 1024)
        .spawn(run)
        .unwrap();

    child.join().unwrap();
}

fn run() {
    
}

以上程式,我們在「main」函數中建立了一個堆疊空間大小為「8 MiB」的執行緒,並且讓這個執行緒去執行「run」這個函數。接著來試試看在「run」函數內,分別加上以下兩行的程式碼後再執行程式,觀察會有什麼樣的結果:

println!("{}", BA_C[0]);
println!("{}", BA_S[0]);

我們可以發現,分別加上以上兩行程式後,執行結果都是:

0

嗯?所以呢?這很正常呀!先別急,我們把以上兩行程式再分別改成以下的樣子後,再執行看看,觀察會有什麼樣的結果:

println!("{}", BA_C[0]);
println!("{}", BA_C[0]);
println!("{}", BA_S[0]);
println!("{}", BA_S[0]);

用Rust 1.44之前的版本執行的話,會發現「BA_C」常數在「run」函數內出現兩次的話,程式就會直接因堆疊溢出(overflow)而中止。但是「BA_S」靜態變數不管在「run」函數內出現幾次,都不會有堆疊溢出的問題發生。

這是因為靜態變數永遠會去使用同一個記憶體中的實體,而常數則是在每次使用時都要將值在記憶體中初始化。[u64; 1000000]這樣的陣列大概會佔用「7.6 MiB」的記憶體空間(堆疊空間),因此在「run」函數中使用兩次以上的「BA_C」常數,就會造成堆疊溢出。

再來看看以下程式:

const VALUE_C: u64 = 10;
static VALUE_S: u64 = 10;

const VALUE_C_A: [u64; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
static VALUE_S_A: [u64; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

fn main(){}

我們其實就可以猜出「VALUE_C」和「VALUE_S」的存取效能應該差不多,但「VALUE_S_A」會比「VALUE_C_A」的存取效能要來得好。

實測看看?

為了驗證我們的猜測,就要實際寫一段程式來測試運算效能啦!這段程式可以在GitHub上取得:

然而,根據實測結果,我們可能會發現無論常數或靜態變數是否為陣列,這兩者的效能都是差不多的,這是因為編譯器有去對常數做優化,在同一個堆疊空間下不會有重複初始化的問題。不過在程式開發階段,我們通常不會去啟用編譯器的優化功能,所以遇到這種會需要經常被使用的不可變陣列,最好還是養成好習慣,使用靜態變數代替常數吧!不過在Rust 1.45之後,常數就比較沒有這樣的問題了(還沒遇到案例)。