Rust 和 C 語言中的神奇內存優化

在編程中因爲缺乏對內存佈局的瞭解,我們就浪費了如此多的內存,這真是太令人抓狂了。

讓我們回顧一下編程語言中一些基本類型的內存大小 (以 Rust 爲例):

既然我們知道每個基本類型的字節大小,那麼問一個問題。以下結構體的字節大小是多少?

struct Foo {
    elem:  u32,
    other: u16,
}

如果是 6,就不會問你這個問題了。雖然 6 並不是完全不正確——因爲結構體對於 u32 包含 4 個字節,對於 u16 包含 2 個字節——結構體的大小是通過對齊最大的字段並將總大小四捨五入到最接近的倍數來計算的。如果這不是很清楚,讓我們看一個簡單的表示。

現在讓我們猜測下面結構體的內存大小 (以字節爲單位):

struct Bar {
    num:     u16,
    bigger:  u32,
    another: u16,
}

如果我們遵循前面描述的心智模型,Bar 結構體的大小將是 12。如果我們考慮結構體的字段的最大對齊,即 4,並將所有其他字段的大小四捨五入爲 4,我們得到: 4 4 4 = 12。

結果如下:(2 + 2) + 4 = 8。似乎我們通過重新排列結構體的字段優化了空閒空間,事實上,Rust 編譯器足夠聰明,可以自動完成這項工作,但並不是所有強類型語言都是如此!讓我們打印 Rust 結構體的內存大小,然後對 C 中的等效結構體做同樣的事情:

Rust 結構體在內存中的大小:

std::mem::size_of::<Bar>() // output: 8

C 結構體在內存中的大小:

struct Bar {
    uint16_t num;
    uint32_t bigger;
    uint16_t another;
};

sizeof(struct Bar) // output: 12

似乎 Rust 編譯器優化了結構體,而 C 編譯器沒有!這是因爲 C 程序員真的需要理解計算機是如何在底層 (ABI) 工作的,不像 Rust 程序員(只是開玩笑😳)… 更嚴重的是,Rust 編譯器是高度優化的,併爲你處理這些,但在 C 中簡單的手動重新排列將達到相同的結果。

Rust 編譯器會高度優化你指示它優化的任務。這並不意味着在 Rust 中你可以忽略數據佈局,理解和考慮數據佈局仍然很重要。

下面是當前結構體數據佈局的可視化表示:

現在,讓我們重新排序 C 結構體的字段並打印它的大小:

struct Bar {
    uint32_t bigger;
    uint16_t num;
    uint16_t another;
};

sizeof(struct Bar) // output: 8

讓我們繼續探索枚舉的數據佈局,你知道 Rust 中枚舉的大小嗎?

enum HtmlTag {
    H1,
    H2,
    UnorderedList,
    OrderedList,
    ...
}

std::mem::size_of::<HtmlTag>() // output: 1

請注意,我們所討論的枚舉除了每個字段的區別外不攜帶任何數據。在本例中,此類枚舉的大小爲 1 字節。現在,假設我們正在構建一個 HTML 標記器—一個循環遍歷 HTML 源代碼文件並提取文件中所有特定 HTML 標記及其位置的程序。簡化結構如下:

struct HtmlToken {
    start_position: u32,
    token_tag:      HtmlTag, 
}

std::mem::size_of::<HtmlToken>() // output: 8

該結構體的大小是 8 字節!u32 爲 4 字節,enum 爲 1 字節,填充爲 3 字節。現在,假設我們對一個大型 HTML 網頁進行標記,並生成 10,000 個標記。

在這種情況下,內存中總大小爲 10,000 * 8 = 80,000 字節,其中填充 30,000 字節🥵。這是對內存的巨大浪費,而且這種情況比你想象的要頻繁得多。

布爾值也有 1 字節的大小。例如,如果你有一個帶有整數和布爾值的結構體,它可能會爲該結構體的每個實例生成 3 個字節的填充。

在本例中,解決方案是將枚舉存儲在帶外。我們將使用由多個數組組成的數據結構。這意味着我們將使用兩個數組並使用索引並行訪問它們。在這種設計下,我們不會爲每個新實例生成填充:

struct HtmlTokens {
    start_positions: [u32; 1],
    token_tags:      [HtmlTag; 1]}

std::mem::size_of::<HtmlTokens>() // output: 5

使用多數組技術,我們將每個 HTML 令牌的 8 字節實例轉換爲兩個數組,每個數組只包含 5 字節的實例! 以我們的 HTML 標記器爲例,這種方法將內存中的標記列表大小減少了 40%…… 對於這樣簡單的修改來說,這真是令人印象深刻。

總結

在這篇文章中介紹了兩種技術來幫助你優化結構體內存大小:結構體中字段的順序可以顯著影響數據佈局和編譯器優化;將布爾值和 1 字節枚舉存儲在帶外,以避免不必要的填充。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3DNNAYe9CTB_ACukaS9_LQ