0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

淺聊Rust程序內存布局

jf_wN0SrCdH ? 來源:Rust語言中文社區(qū) ? 2023-11-01 16:44 ? 次閱讀

淺聊Rust程序內存布局

內存布局看似是底層和距離應用程序開發(fā)比較遙遠的概念集合,但其對前端應用的功能實現頗具現實意義。從WASM業(yè)務模塊至Nodejs N-API插件,無處不涉及到FFI跨語言互操作。甚至,做個文本數據的字符集轉換也得FFI調用操作系統(tǒng)鏈接庫libiconv,因為這意味著更小的.exe/.node發(fā)布文件。而C ABI與內存布局正是跨(計算機)語言數據結構的基礎。

大約兩個月前,在封裝FFI閉包(不是函數指針)過程中,我重新梳理了Rust內存布局知識點。然后,就有沖動寫這么一篇長文。今恰逢國慶八天長假,匯總我之所知與大家分享。開始正文...

存儲寬度size與對齊位數alignment— 內存布局的核心參數

變量值在內存中的存儲信息包含兩個重要屬性

首字節(jié)地址address

存儲寬度size

而這兩個值都不是在分配內存那一刻的“即興選擇”。而是,遵循著一組規(guī)則:

address與size都必須是【對齊位數alignment】的自然數倍。比如說,

對齊位數alignment等于1字節(jié)的變量值可保存于任意內存地址address上。

對齊位數alignment等于2字節(jié)且有效數據長度等于3字節(jié)的變量值

存儲寬度size等于0字節(jié)的變量值可接受任意正整數作為其對齊位數alignment— 慣例是1字節(jié)。

僅能保存于偶數位的內存地址address上。

存儲寬度size也得是4字節(jié) — 從有效長度3字節(jié)到存儲寬度4字節(jié)的擴容過程被稱作“對齊”。

對齊位數alignment必須是2的自然數次冪。即,alignment = 2 ^ N且N是? 29的自然數。

存儲寬度size是有效數據長度對齊填充位數的總和字節(jié)數 — 這一點可能有點兒反直覺。

address,size與alignment的計量單位都是“字節(jié)”。

正是因為address,size與alignment之間存在倍數關系,所以程序對內存空間的利用一定會出現冗余與浪費。這些被浪費掉的“邊角料”則被稱作【對齊填充alignment padding】。對齊填充的計量單位也是字節(jié)。根據“邊角料”出現的位置不同,對齊填充alignment padding又分為

端填充Little-Endian padding—0填充位出現在有效數據側的

端填充Big-Endian padding—0填充位出現在有效數據側的

文字抽象,圖直觀。一圖抵千詞,請看圖

07beb63c-699d-11ee-939d-92fbcf53809c.png

延伸理解:借助于對齊位數,物理上一維的線性內存被重構為了邏輯上N維的存儲空間。不嚴謹地講,一個數據類型 ? 對應一個對齊位數值 ? 按一個【單位一】將內存空間均分一遍 ? 形成一個僅存儲該數據類型值(且只存在于算法與邏輯中)的維度空間。然后,在保存該數據類型的新值時,只要

選擇進入正確的維度空間

跳過已被占用的【單位一】(這些【單位一】是在哪一個維度空間被占用、是被誰占用和怎么占用并不重要)

尋找連續(xù)出現且數量足夠的【單位一】

就行了。

如果【對齊位數alignment】與【存儲寬度size】在編譯時已知,那么該類型就是【靜態(tài)分派】Fixed Sized Type。于是,

類型的對齊位數可由std::()讀取

類型的存儲寬度可由std::()讀取

若【對齊位數alignment】與【存儲寬度size】在運行時才可計算知曉,那么該類型就是【動態(tài)分派】Dynamic Sized Type。于是,

的對齊位數可由std::(&T)讀取

的存儲寬度可由std::(&T)讀取

存儲寬度size的對齊計算

若變量值的有效數據長度payload_size正好是該變量類型【對齊位數alignment】的自然數倍,那么該變量的【存儲寬度size】就是它的【有效數據長度payload_size】。即,size = payload_size;。 否則,變量的【存儲寬度size】就是要大于等于【有效數據長度payload_size】,是【對齊位數alignment】自然數倍的最小數值。

這個計算過程的偽碼描述是


variable.size = variable.payload_size.next_multiple_of(variable.alignment);

這個計算被稱作“(自然數倍)對齊”。

簡單內存布局

基本數據類型

基本數據類型包括bool,u8,i8,u16,i16,u32,i32,u64,i64,u128,i128,usize,isize,f32,f64和char。它們的內存布局在不同型號的設備上略有差異

在非x86設備上,存儲寬度size= 對齊位數alignment(即,倍數N = 1)

在x86設備上,因為設備允許的最大對齊位數不能超過4字節(jié),所以alignment ? 4 Byte

u64與f64的size = alignment * 2(即,N = 2)。

u128與i128的size = alignment * 4(即,N = 4)。

其它基本數據類型依舊size = alignment(即,倍數N = 1)。

FST瘦指針

瘦指針的內存布局與usize類型是一致的。因此,在不同設備和不同架構上,其性能表現略有不同

在非x86的

32位架構上,size = alignment = 4 Byte(N = 1)

64位架構上,size = alignment = 8 Byte(N = 1)

在x86的

size = 8 Byte

alignment = 4 Byte—x86設備最大對齊位數不能超過4字節(jié)

N = 2

32位架構上,size = alignment = 4 Byte(N = 1)

64位設備上,

DST胖指針

胖指針的存儲寬度size是usize類型的倍,對齊位數卻與usize相同。就依賴于設備/架構的性能表現而言,其與瘦指針行為一致:

在非x86的

size = 16 Byte

alignment = 8 Byte

N = 2

size = 8 Byte

alignment = 4 Byte

N = 2

32位架構上,

64位架構上,

在x86的

size = 16 Byte

alignment = 4 Byte—x86設備最大對齊位數不能超過4字節(jié)

N = 4

size = 8 Byte

alignment = 4 Byte

N = 2

32位架構上,

64位設備上,

數組[T; N],切片[T]和str

str就是滿足UTF-8編碼規(guī)范的增強版[u8]切片。 存儲寬度size是全部元素存儲寬度之


array.size = std::() * array.len(); 對齊位數alignment與單個元素的對齊位數一致。

array.alignment = std::();

()單位類型

存儲寬度size=0 Byte 對齊位數alignment=1 Byte 所有零寬度數據類型都是這樣的內存布局配置。 來自【標準庫】的零寬度數據類型包括但不限于:

()單位類型 — 模擬“空”。

std::PhantomData— 繞過“泛型類型形參必須被使用”的編譯規(guī)則。進而,成就類型狀態(tài)設計模式中的Phantom Type。

std::PhantomPinned— 禁止變量值在內存中“被挪來挪去”。進而,成就異步編程中的“自引用結構體self-referential struct”。

自定義數據結構的內存布局

復合數據結構的內存布局描繪了該數據結構(緊內一層)字段的內存位置“擺放”關系(比如,間隙與次序等)。在層疊嵌套的數據結構中,內存布局都是就某一層數據結構而言的。它既承接不了來自外層父數據結構的內存布局,也決定不了更內層子數據結構的內存布局,更代表不了整個數據結構內存布局的總覽。舉個例子,


#[repr(C)] struct Data { id: u32, name: String } #[repr(C)]僅只代表最外層結構體Data的兩個字段id和name是按C內存布局規(guī)格“擺放”在內存中的。但,#[repr(C)]并不意味著整個數據結構都是C內存布局的,更改變不了name字段的String類型是Rust內存布局的事實。若你的代碼意圖是定義完全C ABI的結構體,那么【原始指針】才是該用的類型。

use ::{c_char, c_uint}; #[repr(C)] struct Data { id: c_uint, name: *const c_char // 注意對比 }

內存布局核心參數

自定義數據結構的內存布局包含如下五個屬性

alignment

定義:數據結構自身的對齊位數

規(guī)則:

alignment=2的n次冪(n是? 29的自然數)

不同于基本數據類型alignment = size,自定義數據結構alignment的算法隨不同的數據結構而相異。

size

定義:數據結構自身的寬度

規(guī)則:size必須是alignment自然數倍。若有效數據長度payload_size不足size,就添補空白【對齊填充位】湊足寬度。

field.alignment

定義:每個字段的對齊位數

規(guī)則:field.alignment=2的n次冪(n是? 29的自然數)

field.size

定義:每個字段的寬度

規(guī)則:field.size必須是field.alignment自然數倍。若有效數據長度field.payload_size不足field.size,就添補空白【對齊填充位】湊足寬度。

field.offset

定義:每個字段首字節(jié)地址相對于上一層數據結構首字節(jié)地址的偏移字節(jié)數

規(guī)則:

field.offset必須是field.alignment自然數倍。若不足,就墊入空白【對齊填充位】和向后推移當前字段的起始位置。

前一個字段的field.offset + field.size ?后一個字段的field.offset

自定義枚舉類enum的內存布局一般與枚舉類分辨因子discriminant的內存布局一致。更復雜的情況,請見下文章節(jié)。

預置內存布局方案

編譯器內置了四款內存布局方案,分別是

默認Rust內存布局 — 沒有元屬性注釋

C內存布局#[repr(C)]

數字類型·內存布局#[repr(u8 / u16 / u32 / u64 / u128 / usize / i8 / i16 / i32 / i64 / i128 / isize)]

僅適用于枚舉類。

支持與C內存布局混搭使用。比如,#[repr(C, u8)]。

透明·內存布局#[repr(transparent)]

僅適用于單字段數據結構。

預置內存布局方案對比

相較于C內存布局,Rust內存布局面向內存空間利用率做了優(yōu)化— 省內存。具體的技術手段包括Rust編譯器

重排了字段的存儲順序,以盡可能多地消減掉“邊角料”(對齊填充)占用的字節(jié)位數。于是,在源程序中字段聲明的詞法次序經常不同于【運行時】它們在內存里的實際存儲順序。

允許多個零寬度字段共用一個內存地址。甚至,零寬度字段也被允許與普通(有數據)字段共享內存地址。

以C ABI中間格式為橋的C內存布局雖然實現了Rust跨語言數據結構,但它卻更費內存。這主要出于兩個方面原因:

C內存布局未對字段存儲順序做優(yōu)化處理,所以字段在源碼中的詞法順序就是它們在內存條里的存儲順序。于是,若 @程序員 沒有拿著算草紙和數著比特位“人肉地”優(yōu)化每個數據結構定義,那么由對齊填充位冗余造成的內存浪費不可避免。

C內存布局不支持零寬度數據類型。零寬度數據類型是Rust語言設計的重要創(chuàng)新。相比之下,

(參見C17規(guī)范的第6.7.2.1節(jié))字段結構體會導致標準C程序出現U.B.,除非安裝與開啟GNU的C擴展。

Cpp編譯器會強制給無字段結構體安排一個字節(jié)寬度,除非該數據結構被顯式地標記為[[no_unique_address]]。

以費內存為代價,C內存布局賦予Rust數據結構的另一個“超能力”就是:“僅通過變換【指針類型】就可將內存上的一段數據重新解讀為另一個數據類型的值”。比如,void * / std::c_void被允許指向任意數據類型的變量值例程。但在Rust內存布局下,需要調用專門的標準庫函數std::transmute()才能達到相同的目的。 除了上述鮮明的差別之外,C與Rust內存布局都允許【對齊位數alignment】參數被微調,而不一定總是全部字段alignment中的最大值。這包括但不限于:

修飾符align(x)增加alignment至指定值。例如,#[repr(C, align(8))]將C內存布局中的【對齊位數】上調至8字節(jié)

修飾符packed(x)減小alignment至指定值。例如,#[repr(packed)]將默認Rust內存布局中的【對齊位數】下調至1字節(jié)

結構體struct的C內存布局

結構體算是最“中規(guī)中矩”的數據結構。無論是否對結構體的字段重新排序,只要將它們一個不落地鋪到內存上就完成一多半功能了。所以,結構體存儲寬度struct.size是全部字段size之再(自然數倍)對齊于【結構體對齊位數struct.alignment】的結果。有點抽象上偽碼


struct.size = struct.fields().map(|field| field.size).sum() // 第一步,求全部字段寬度值之和 .next_multiple_of(struct.alignment); // 第二步,求既大于等于【寬度值之和】,又是`struct.alignment`自然數倍的最小數值 相較于Rust內存布局優(yōu)化算法的錯綜復雜,我好似只能講清楚C內存布局的始末: 首先,結構體自身的對齊位數struct.alignment就是全部字段對齊位數field.alignment中的最大值。

struct.alignment = struct.fields().map(|field| field.alignment).max(); 其次,聲明一個(可修改的)游標變量offset_cursor以實時跟蹤(參照于結構體首字節(jié)地址的)字節(jié)偏移量。游標變量的初始值為0表示該游標與結構體的內存起始位置重合。

let mut offset_cursor = 0; 接著,沿著源碼中字段的聲明次序,逐一處理各個字段:

【對齊】若游標變量值offset_cursor不是當前字段對齊位數field.alignment的自然數倍(即,未對齊),就計算大于等于offset_cursor是field.alignment自然數倍的最小數值。并將計算結果更新入游標變量offset_cursor,以插入填充位對齊和向后推移字段在內存中的”擺放“位置。


if offset_cursor.rem_euclid(field.alignment) > 0 { offset_cursor = offset_cursor.next_multiple_of(field.alignment); }

【定位】當前游標的位置就是該字段的首字節(jié)偏移量


field.offset = offset_cursor;

跳過當前字段寬度field.size— 遞歸算法求值數據結構的存儲寬度。字段子數據結構的內存布局對上一層父數據結構是黑盒的。


offset_cursor += field.size

繼續(xù)處理下一個字段。

然后,在結構體內全部字段都被如上處理之后,

【對齊】若游標變量值offset_cursor不是結構體對齊位數struct.alignment的自然數倍(即,未對齊),就計算大于等于offset_cursor是struct.alignment自然數倍的最小數值。并將計算結果更新入游標變量offset_cursor,以增補填充位對齊和擴容有效數據長度至結構體存儲寬度。


if offset_cursor.rem_euclid(struct.alignment) > 0 { offset_cursor = offset_cursor.next_multiple_of(struct.alignment); }

【定位】當前游標值就是整個結構體的寬度(含全部對齊填充位)


struct.size = offset_cursor;

至此,結構體的C內存布局結束。然后,std::GlobalAlloc就能夠拿著這套“策劃案”向操作系統(tǒng)申請內存空間去了。由此可見,每次【對齊】處理都會在有效數據周圍“埋入”大量空白“邊角料”(學名:對齊填充位alignment padding)。但出于歷史原因,為了完成與其它計算機語言的FFI互操作,這些浪費還是必須的。下面附以完整的偽碼輔助理解


// 1. 結構體的【對齊位數】就是它的全部字段【對齊位數】中的最大值。 struct.alignment = struct.fields().map(|field| field.alignment).max(); // 2. 聲明一個游標變量,以實時跟蹤(相對于結構體首字節(jié)地址)的偏移量。 let mut offset_cursor = 0; // 3. 按照字段在源代碼中的詞法聲明次序,逐一遍歷每個字段。 for field in struct.fields_in_declaration_order() { if offset_cursor.rem_euclid(field.alignment) > 0 { // 4. 需要【對齊】當前字段 offset_cursor = offset_cursor.next_multiple_of(field.alignment); } // 5. 【定位】字段的偏移量就是游標變量的最新值。 field.offset = offset_cursor; // 6. 在跳過當前字段寬度的字節(jié)長度(含對齊填充字節(jié)數) offset_cursor += field.size; } if offset_cursor.rem_euclid(struct.alignment) > 0 { // 7. 需要【對齊】結構體自身 offset_cursor = offset_cursor.next_multiple_of(struct.alignment); } // 8. 【定位】結構體的寬度(含對齊填充字節(jié)數)就是游標變量的最新值。 struct.size = offset_cursor;

聯合體union的C內存布局

形象地講,聯合體是給內存中同一段字節(jié)序列準備了多套“數據視圖”,而每套“數據視圖”都嘗試將該段字節(jié)序列解釋為不同數據類型的值。所以,無論在聯合體內聲明了幾個字段,都僅有一個字段值會被保存于物理存儲之上。從原則上講,聯合體union的內存布局一定與占用內存最多的字段一致,以確保任何字段值都能被容納。從實踐上講,有一些細節(jié)處理需要斟酌:

聯合體的對齊位數union.alignment等于全部字段對齊位數中的最大值(同結構體)。


union.alignment = union.fields().map(|field| field.alignment).max();

聯合體的存儲寬度union.size是最長字段寬度值longest_field.size(自然數倍)對齊于聯合體自身對齊位數union.alignment的結果。有點抽象上偽碼


union.size = union.fields().map(|field| field.size).max() // 第一步,求最長字段的寬度值 .next_multiple_of(union.alignment); // 第二步,求既大于等于【最長字段寬度值】,又是`union.alignment`自然數倍的最小數值

舉個例子,聯合體Example0內包含了u8與u16類型的兩個字段,那么Example0的內存布局就一定與u16的內存布局一致。再舉個例子,


use ::mem; #[repr(C)] union Example1 { f1: u16, f2: [u8; 4], } println!("alignment = {1}; size = {0}", mem::(), mem::()) 看答案之前,不防先心算一下,程序向標準輸出打印的結果是多少。演算過程如下:

字段f1的

存儲寬度size是2字節(jié)。

對齊位數alignment也是2字節(jié),因為基本數據類型的【對齊位數alignment】就是它的【存儲寬度size】。

字段f2的

存儲寬度size是4字節(jié),因為數組的【存儲寬度size】就是全部元素存儲寬度之。

對齊位數alignment是1字節(jié),因為數組的【對齊位數alignment】就是元素的【對齊位數alignment】。

聯合體Example1的

對齊位數alignment就是2字節(jié),因為取最大值

存儲寬度size是4字節(jié),因為得取最大值

再來一個更復雜點兒的例子,


use ::mem; #[repr(C)] union Example2 { f1: u32, f2: [u16; 3], } println!("alignment = {1}; size = {0}", mem::(), mem::()) 同樣,在看答案之前,不防先心算一下,程序向標準輸出打印的結果是多少。演算過程如下:

字段f1的存儲寬度與對齊位數都是4字節(jié)。

字段f2的

對齊位數是2字節(jié)。

存儲寬度是6字節(jié)。

聯合體Example2的

對齊位數alignment是4字節(jié) — 取最大值,沒毛病。

存儲寬度size是8字節(jié),因為不僅得取最大值6字節(jié),還得向Example2.alignment自然數倍對齊。于是,才有了額外2字節(jié)的【對齊填充】和擴容【聯合體】有效長度6字節(jié)至存儲寬度8字節(jié)。你猜對了嗎?

不經意的巧合

思維敏銳的讀者可以已經注意到:單字段【結構體】與單字段【聯合體】的內存布局是相同的,因為數據結構自身的內存布局就是唯一字段的內存布局。不信的話,執(zhí)行下面的例程試試


use ::mem; #[repr(C)] struct Example3 { f1: u16 } #[repr(C)] union Example4 { f1: u16 } // struct 內存布局 等同于 union 的內存布局 assert_eq!(mem::(), mem::()); assert_eq!(mem::(), mem::()); // struct 內存布局 等同于 u16 的內存布局 assert_eq!(mem::(), mem::()); assert_eq!(mem::(), mem::());

枚舉類enum的C內存布局

突破“枚舉”字面含義的束縛,Rust的創(chuàng)新使Rust enum與傳統(tǒng)計算機語言中的同類項都不同。Rust枚舉類

既包括:C風格的“裝”枚舉 — 僅標記狀態(tài),卻不記錄細節(jié)數據。

也支持:Rust風格的“裝”枚舉 — 標記狀態(tài)的同時也記錄細節(jié)數據。

在Rust References一書中,

裝”枚舉被稱為“無字段·枚舉類 field-less enum”或“僅單位類型·枚舉類 unit-only enum”。

裝”枚舉被別名為“伴字段·枚舉類enum with fields”。

在Cpp程序中,需要借助【標準庫】的Tagged Union數據結構才能模擬出同類的功能來。欲了解更多技術細節(jié),推薦讀我的另一篇文章。

禁忌:C內存布局的枚舉類必須至少包含一個枚舉值。否則,編譯器就會報怨:error[E0084]: unsupported representation for zero-variant enum。

“輕裝”枚舉類的內存布局

因為“輕裝”枚舉值的唯一有效數據就是“記錄了哪個枚舉項被選中的”分辨因子discriminant,所以枚舉類的內存布局就是枚舉類【整數類型】分辨因子的內存布局。即,


LightEnum.alignment = discriminant.alignment; // 對齊位數 LightEnum.size = discriminant.size; // 存儲寬度 別慶幸!故事遠沒有看起來這么簡單,因為【整數類】是一組數字類型的總稱(餒餒的“集合名詞”)。所以,它包含但不限于 

Rust C 存儲寬度
u8 / i8 unsigned char / char 單字節(jié)
u16 / i16 unsigned short / short 雙字節(jié)
u32 / i32 unsigned int / int 四字節(jié)
u64 / i64 unsigned long / long 八字節(jié)
usize / isize 沒有概念對等項,可能得元編程 等長于目標架構“瘦指針”寬度

維系FFI兩端Rust和C枚舉類分辨因子都采用相同的整數類型才是最“坑”的,因為

C / Cpp enum實例可存儲任意類型的整數值(比如,char,short,int和long)— 部分原因或許是C系語法靈活的定義形式:“typedef enum塊 + 具名常量”。所以,C / Cpp enum非常適合被做成“比特開關”。但在Rust程序中,就不得不引入外部軟件包bitflags了。

C內存布局Rust枚舉類分辨因子discriminant只能是i32類型— 【存儲寬度size】是固定的4字節(jié)。

Rust內存布局·枚舉類·分辨因子discriminant的整數類型是編譯時由rustc決定的,但最寬支持到isize類型。

這就對FFI - C端的程序設計提出了額外的限制條件:至少,由ABI接口導出的枚舉值得用int類型定義。否則,Rust端FFI函數調用就會觸發(fā)U.B.。FFI門檻稍有上升。 扼要歸納:

FFI - Rust端C內存布局的枚舉類對FFI - C端枚舉值的【整數類型】提出了“確定性假設invariant”:枚舉值的整數類型是int且存儲寬度等于4字節(jié)。

C端 @程序員 必須硬編碼所有枚舉值的數據類型,以滿足該假設。

FFI跨語言互操作才能成功“落地”,而不是發(fā)生U.B.。

來自C端的遷就固然令人心情愉悅,但新應用程序難免要對接兼容遺留系統(tǒng)與舊鏈接庫。此時,再給FFI - C端提要求就不那么現實了 — 深度改動“屎山”代碼風險巨大,甚至你可能都沒有源碼?!?strong>數字類型·內存布局】正是解決此棘手問題的技術方案:

以【元屬性】#[repr(整數類型名)]注釋枚舉類定義

明確指示Rust編譯器采用給定【整數類型】的內存布局,組織【分辨因子discriminant】的數據存儲,而不總是遵循i32內存布局。

從C / Cpp整數類型至Rust內存布局元屬性的映射關系包括但不限于

C Rust 元屬性
unsigned char / char #[repr(u8)] / #[repr(i8)]
unsigned short / short #[repr(u16)] / #[repr(i16)]
unsigned int / int #[repr(u32)] / #[repr(i32)]
unsigned long / long #[repr(u64)] / #[repr(i64)]

舉個例子,


use ::mem; #[repr(C)] enum Example5 { // ”輕裝“枚舉類,因為 A(), // field-less variant B {}, // field-less variant C // unit variant } println!("alignment = {1}; size = {0}", mem::(), mem::()); 上面代碼定義的是C內存布局的“裝”枚舉類Example5,因為它的每個枚舉值不是“無字段”,就是“單位類型”。于是,Example5的內存布局就是i32類型的alignment = size = 4 Byte。 再舉個例子,

use ::mem; #[repr(u8)] enum Example6 { // ”輕裝“枚舉類,因為 A(), // field-less variant B {}, // field-less variant C // unit variant } println!("alignment = {1}; size = {0}", mem::(), mem::()); 上面代碼定義的是【數字類型·內存布局】的“裝”枚舉類Example6。它的內存布局是u8類型的alignment = size = 1 Byte。

“重裝”枚舉類的內存布局

【“重裝”枚舉類】絕對是Rust語言設計的一大創(chuàng)新,但同時也給FFI跨語言互操作帶來了嚴重挑戰(zhàn),因為在其它計算機語言中沒有概念對等的核心語言元素“接得住它”。對此,在做C內存布局時,編譯器rustc會將【“重裝”枚舉類】“降維”成一個雙字段結構體:

第一個字段是:剝去了所有字段的【“輕裝”枚舉】,也稱【分辨因子枚舉類Discriminant enum】。

第二個字段是:由枚舉值variant內字段fields拼湊成的【結構體struct】組成的【聯合體union】。

前者記錄選中項的“索引值” — 誰被選中;后者記憶選中項內的值:根據索引值,以對應的數據類型,讀/寫聯合體實例的字段值。 文字描述著實有些晦澀與抽象。邊看下圖,邊再體會。一圖抵千詞?。P鍵還是對union數據類型的理解)

07ce908e-699d-11ee-939d-92fbcf53809c.png

上圖中有三個很細節(jié)的知識點容易被讀者略過,所以在這里特意強調一下:

保存枚舉值字段的結構體struct A / B / C都派生了trait Copy,派生了trait Clone,因為

union數據結構要求它的每個字段都是可復制

同時,trait Copy又是trait Clone的subtrait

降維后結構體struct Example7內的字段名不重要,但字段排列次序很重要。因為在C ABI中,結構體字段的存儲次序就是它們在源碼中的聲明次序,所以Cpp標準庫中的Tagged Union數據結構總是,根據約定的字段次序,

將第一個字段解釋為“選中項的索引號”,

將第二個字段解讀為“選中項的數據值”。

C內存布局的分辨因子枚舉類enum Discriminant的分辨因子discriminant依舊是i32類型值,所以FFI - C端的枚舉值仍舊被要求采用int整數類型。

舉個例子,


use ::mem; #[repr(C)] enum Example8 { Variant0(u8), Variant1, } println!("alignment = {1}; size = {0}", mem::(), mem::()) 看答案之前,不防先心算一下,程序向標準輸出打印的結果是多少。演算過程如下:

enum被“降維”成struct

就C內存布局而言,struct的alignment是全部字段alignment中的最大值。

字段union.Variant0是字段元組結構體,且字段類型是基本數據類型。所以,union.Variant0.alignment = union.Variant0.size = 1 Byte

字段union.Variant1是單位類型。所以,union.Variant1.alignment = 1 Byte和union.Variant1.size = 0 Byte

于是,union.alignment = 1 Byte

字段tag是C內存布局的“輕裝”枚舉類。所以,tag.alignment = tag.size = 4 Byte

字段union是union數據結構。所以,union的alignment也是全部字段alignment中的最大值。

于是,struct.alignment = 4 Byte

struct的size是全部字段size之和。

union.Variant0.size = 1 Byte

union.Variant1.size = 0 Byte

于是,union.size = 1 Byte

字段tag是C內存布局的“輕裝”枚舉類。所以,tag.size = 4 Byte

字段union是union數據結構。union的size是全部字段size中的最大值。

于是,不精準地struct.size ≈ 5 Byte(約等)

此刻struct.size并不是struct.alignment的自然數倍。所以,需要給struct增補“對齊填充位”和向struct.alignment自然數倍對齊

于是,struct.size = 8 Byte(直等)

哎!看見沒,C內存布局還是比較費內存的,一少半都是空白“邊角料”。 【“重裝”枚舉類】同樣會遇到FFI - ABI兩端【Rust枚舉類分辨因子discriminant】與【C枚舉值】整數類型一致約定的難點。為了遷就C端遺留系統(tǒng)和舊鏈接庫對枚舉值【整數類型】的選擇,Rust編譯器依舊選擇“降維”處理enum。但,這次不是將enum變形成struct,而是跳過struct封裝和直接以union為“話事人”。同時,將【分辨因子·枚舉值】作為union字段數據結構的個字段:

元組枚舉值,分辨因子就是數據結構第0個元素

結構體枚舉值,分辨因子就數據結構第一個字段。注:字段名不重要,字段次序更重要。

文字描述著實有些晦澀與抽象。邊看下圖,邊對比上圖,邊體會。一圖抵千詞!

07d701a6-699d-11ee-939d-92fbcf53809c.png

由上圖可見,C與【數字類型】的混合內存布局

既保證了降級后union與struct數據結構繼續(xù)滿足C ABI的存儲格式要求。

又確保了【Rust端枚舉類分辨因子】與【C端枚舉值】之間整數類型的一致性。

舉個例子,假設目標架構是32位系統(tǒng),


use ::mem; #[repr(C, u16)] enum Example10 { Variant0(u8), Variant1, } println!("alignment = {1}; size = {0}", mem::(), mem::()) 看答案之前,不防先心算一下,程序向標準輸出打印的結果是多少。演算過程如下:

enum被“降維”成union

union的alignment是全部字段alignment中的最大值。

第一個字段是u16類型的分辨因子枚舉值。所以,Variant0.0.alignment = Variant0.0.size = 2 Byte

第二個字段是u8類型數字。所以,Variant0.1.alignment = Variant0.1.size = 1 Byte

于是,union.Variant0.alignment = 2 Byte

字段union.Variant0是雙字段元組結構體。所以,struct的alignment是全部字段alignment中的最大值。

字段union.Variant1是字段元組結構體且唯一字段就是u16分辨因子枚舉值。所以,union.Variant1.alignment = union.Variant1.size = 2 Byte

于是,union.alignment = 2 Byte

union的size是全部字段size中的最大值。

第一個字段是u16類型的分辨因子枚舉值。所以,Variant0.0.size = 2 Byte

第二個字段是u8類型數字。所以,Variant0.1.size = 1 byte

于是,不精準地union.Variant0.size ≈ 3 Byte(約等)

此刻union.Variant0.size不是union.Variant0.alignment的自然數倍。所以,需要對union.Variant0增補“對齊填充位”和向union.Variant0.alignment自然數倍對齊

于是,union.Variant0.size = 4 Byte(直等)

字段union.Variant0是雙字段元組結構體。所以,struct的size是全部字段size之。

字段union.Variant1是字段元組結構體且唯一字段就是u16分辨因子枚舉值。所以,union.Variant1.size = 2 Byte

于是,union.size = 4 Byte

哎!看見沒,C 內存布局還是比較費內存的,一少半的“邊角料”。

新設計方案好智慧

優(yōu)化掉了一層struct封裝。即,從enum ? struct ? union縮編至enum ? union

將被優(yōu)化掉的struct的職能(— 記錄選中項的“索引值”)合并入了union字段的數據結構中。于是,聯合體的每個字段

既要,保存枚舉值的字段數據 — 舊職能

還要,記錄枚舉值的“索引號” — 新職能

但有趣的是,比較上一版數據存儲設計方案,C內存布局卻沒有發(fā)生變化。邏輯描述精簡了但物理實質未變,這太智慧了!因此,由Cpp標準庫提供的Tagged Union數據結構依舊“接得住”Rust端【“重裝”枚舉值】。

僅【數字類型·內存布局】的“重裝”枚舉類

若不以C【數字類型】的混合內存布局來組織枚舉類enum Example9的數據存儲,而僅保留【數字類型】內存布局,那么上例中被降維后的【聯合體】與【結構體】就都會缺省采用Rust內存布局。參見下圖:

07e4570c-699d-11ee-939d-92fbcf53809c.png

補充于最后,思維活躍的讀者這次千萬別想太多了。沒有#[repr(transparent, u16)]的內存布局組合,因為【透明·內存布局】向來都是“孤來孤往”的。

數字類型·內存布局

僅【枚舉類】支持【數字類型·內存布局】。而且,將無枚舉值的枚舉類注釋為【數字類型·內存布局】會導致編譯失敗。舉個例子


#[repr(u16)] enum Example12 { // 沒有任何枚舉值 } 會導致編譯失敗error[E0084]: unsupported representation for zero-variant enum。

透明·內存布局

“透明”不是指“沒有”,而是意味著:在層疊嵌套數據結構中,外層數據結構的【對齊位數】與【存儲寬度】等于(緊)內層數據結構的【對齊位數】和【存儲寬度】。因此,它僅適用于

單字段的結構體 — 結構體的【對齊位數】與【存儲寬度】等于唯一字段的【對齊位數】和【存儲寬度】。


struct.alignment = struct.field.alignment; struct.size = struct.field.size;

單枚舉值且單字段的“重裝”枚舉類 — 枚舉類的【對齊位數】與【存儲寬度】等于唯一枚舉值內唯一字段的【對齊位數】和【存儲寬度】。


HeavyEnum.alignment = HeavyEnum::variant.field.alignment; HeavyEnum.size = HeavyEnum::variant.field.size;

單枚舉值的“輕裝”枚舉類 — 枚舉類的【對齊位數】與【存儲寬度】等于單位類型的【對齊位數】和【存儲寬度】。


LightEnum.alignment = 1; LightEnum.size = 0;

原則上,數據結構中的唯一字段必須是非零寬度的。但是,若【透明·內存布局】數據結構涉及到了

類型狀態(tài)設計模式

異步多線程

,那么Rust內存布局的靈活性也允許:結構體和“重裝”枚舉值額外包含任意數量的零寬度字段。比如,

std::PhantomData為類型狀態(tài)設計模式,提供Phantom Type支持。

std::PhantomPinned為自引用數據結構,提供!Unpin支持。

舉個例子,


use ::{marker::PhantomData, mem}; #[repr(transparent)] enum Example13 { // 含`Phantom Type`的“重裝”枚舉類 Variant0 ( f32, // 普通有數據字段 PhantomData // 零寬度字段。泛型類型形參未落實到有效數據上。 ) } println!("alignment = {1}; size = {0}", mem::>(), mem::>()) 看答案之前,不防先心算一下,程序向標準輸出打印的結果是多少。演算過程如下:

因為Example14.Variant0.1字段是零寬度數據類型PhantomData,所以它的 和不參與內存布局計算。

alignment = 1 Byte

size = 0 Byte

首字節(jié)地址address與Example10.Variant0.0字段重疊。

因為【透明·內存布局】,所以 外層枚舉類的

【對齊位數】Example14.alignment = Example10::Variant0.0.alignment = 4 Byte

【存儲寬度】Example14.size = Example10::Variant0.0.size = 4 Byte

不同于【數字類型·內存布局】,【透明·內存布局】不被允許與其它內存布局混合使用。比如,

#[repr(C, u16)]是合法的

#[repr(C, transparent)]和#[repr(transparent, u16)]就會導致語編譯失敗

其它類型的內存布局

trait Object與由胖指針&dyn Trait/Box引用的變量值的【內存布局】相同。

閉包Closure沒有固定的【內存布局】。

微調內存布局

只有Rust與C內存布局具備微調能力,且只能修改【對齊位數alignment】參數值。另外,不同數據結構可做的微調操作也略有不同:

struct,union,enum數據結構可調對齊位數

僅struct,union被允許調對齊位數

數據結構【對齊位數alignment】值的增加與減少需要使用不同的元屬性修飾符

#[repr(align(新·對齊位數))]增加對齊位數新值。將小于等于數據結構原本對齊位數的值輸入align(x)修飾符是無效的。

#[repr(packed(新·對齊位數))]減少對齊位數新值。將大于等于數據結構原本對齊位數的值輸入packed(x)修飾符也是無效的。

align(x)與packed(x)修飾符的實參是【目標】字節(jié)數,而不是【增量】字節(jié)數。所以,#[repr(align(8))]指示編譯器增加對齊數至8字節(jié),而不是增加8字節(jié)。另外,新對齊位數必須是2的自然數次冪。

禁忌

同一個數據類型不被允許既增加又減少對齊位數。即,align(x)與packed(x)修飾符不能共同注釋一個數據類型定義。

減小對齊位數的外層數據結構禁止包含增加對齊位數的子數據結構。即,#[repr(packed(x))]數據結構不允許嵌套包含#[repr(align(y))]子數據結構。

枚舉類內存布局的微調

首先,枚舉類不允許調對齊位數。 其次,上調枚舉類的對齊位數也會觸發(fā)“內存布局重構”的負作用。編譯器會效仿Newtypes 設計模式重構#[repr(align(x))] enum枚舉類為嵌套包含了enum的#[repr(align(x))] struct元組結構體。一圖抵千詞,請參閱下圖。

07f2a424-699d-11ee-939d-92fbcf53809c.png

由上圖可見,在內存布局重構之后,C內存布局繼續(xù)保留在枚舉類上,而align(16)修飾符僅對外層的結構體有效。所以,從底層實現來講,枚舉類是不支持內存布局微調的,僅能借助外層的Newtypes數據結構間接限定。 以上面的數據結構為例,


use ::mem; #[repr(C, align(16))] enum Example15 { A, B, C } println!("alignment = {1}; size = {0}", mem::(), mem::()) 看答案之前,不防先心算一下,程序向標準輸出打印的結果是多少。演算過程如下:

因為C內存布局,所以枚舉類的分辨因子是i32類型和枚舉類的存儲寬度size = 4 Byte。

但,align(16)將內存空間占用強制地從alignment = size = 4 Byte提升到alignment = size = 16 Byte。

結束語

這次分享的內容比較多,感謝您耐心地讀到文章結束。文章中問答式例程的輸出結果,您猜對了幾個呀? 內存布局是一個非常宏大技術主題,這篇文章僅是拋磚引玉,講的粒度比較粗,涉及的具體數據結構也都很基礎。更多FFI和內存布局的實踐經驗沉淀與知識點匯總,我將在相關技術線的后續(xù)文章中陸續(xù)分享。

審核編輯:湯梓紅
聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯系本站處理。 舉報投訴
  • 計算機
    +關注

    關注

    19

    文章

    7360

    瀏覽量

    87633
  • 內存
    +關注

    關注

    8

    文章

    2966

    瀏覽量

    73814
  • 程序
    +關注

    關注

    116

    文章

    3756

    瀏覽量

    80754
  • 函數指針
    +關注

    關注

    2

    文章

    56

    瀏覽量

    3770
  • Rust
    +關注

    關注

    1

    文章

    228

    瀏覽量

    6542

原文標題:程序內存布局

文章出處:【微信號:Rust語言中文社區(qū),微信公眾號:Rust語言中文社區(qū)】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    如何在Rust中使用Memcached

    Memcached是一種高性能、分布式的內存對象緩存系統(tǒng),可用于加速動態(tài)Web應用程序。Rust是一種系統(tǒng)級編程語言,具有內存安全、高性能和并發(fā)性等特點。
    的頭像 發(fā)表于 09-19 16:30 ?1181次閱讀

    Rust GUI實踐之Rust-Qt模塊

    Rust-Qt 是 Rust 語言的一個 Qt 綁定庫,它允許 Rust 開發(fā)者使用 Qt 框架來創(chuàng)建跨平臺的圖形界面應用程序。Qt 是一個跨平臺的應用
    的頭像 發(fā)表于 09-30 16:43 ?1496次閱讀

    Rust語言如何與 InfluxDB 集成

    Rust 是一種系統(tǒng)級編程語言,具有高性能和內存安全性。InfluxDB 是一個開源的時間序列數據庫,用于存儲、查詢和可視化大規(guī)模數據集。Rust 語言可以與 InfluxDB 集成,提供高效
    的頭像 發(fā)表于 09-30 16:45 ?1079次閱讀

    如何在Rust中讀寫文件

    Rust是一種系統(tǒng)級編程語言,它的設計目標是提供安全、并發(fā)和高性能的編程體驗。Rust的特點在于其內存安全性和線程安全性,它采用了一些創(chuàng)新性的技術,如所有權系統(tǒng)和生命周期,來解決C和C++中常
    的頭像 發(fā)表于 09-20 10:57 ?1922次閱讀

    Rust 語言中的 RwLock內部實現原理

    Rust是一種系統(tǒng)級編程語言,它帶有嚴格的內存管理、并發(fā)和安全性規(guī)則,因此很受廣大程序員的青睞。RwLock(讀寫鎖)是 Rust 中常用的線程同步機制之一,本文將詳細介紹
    的頭像 發(fā)表于 09-20 11:23 ?812次閱讀

    淺談程序內存布局

    前言1、什么是 User space 與 Kernel space?2、Linux 下一個進程里典型的內存布局是怎樣的?3、什么是棧區(qū)?4、什么是堆區(qū)?5、malloc 算法是如何實現的?6
    發(fā)表于 12-26 01:39

    【原創(chuàng)】內存指針操作

    內存,然后讓變量p指向這塊內存,即p的值是這1024字節(jié)的連續(xù)內存的地址。在程序中就可以通過p來操作這塊內存區(qū)域。在
    發(fā)表于 07-28 09:10

    怎樣去使用Rust進行嵌入式編程呢

    使用Rust進行嵌入式編程Use Rust for embedded development篇首語:Rust的高性能、可靠性和生產力使其適合于嵌入式系統(tǒng)。在過去的幾年里,Rust
    發(fā)表于 12-22 07:20

    RUST在嵌入式開發(fā)中的應用是什么

    Rust是一種編程語言,它使用戶能夠構建可靠、高效的軟件,尤其是用于嵌入式開發(fā)的軟件。它的特點是:高性能:Rust具有驚人的速度和高內存利用率。可靠性:在編譯過程中可以消除內存錯誤。生
    發(fā)表于 12-24 08:34

    Rust原子類型和內存排序

    原子類型在構建無鎖數據結構,跨線程共享數據,線程間同步等多線程并發(fā)編程場景中起到至關重要的作用。本文將從Rust提供的原子類型和原子類型的內存排序問題兩方面來介紹。
    的頭像 發(fā)表于 10-31 09:21 ?899次閱讀

    Rust語言助力Android內存安全漏洞大幅減少

    從 Android 12 開始,Google 就在 Android 系統(tǒng)中帶來了 Rust 語言的支持,作為 C/C++ 的替代方案,他們的目標并不是把現有的 C/C++ 代碼都轉換成為 Rust
    發(fā)表于 12-06 17:56 ?652次閱讀

    JVM內存布局詳解

    JVM內存布局規(guī)定了Java在運行過程中內存申請、分配、管理的策略,保證了JVM的穩(wěn)定高效運行。不同的JVM對于內存的劃分方式和管理機制存在部分差異。結合JVM虛擬機規(guī)范,一起來探討j
    的頭像 發(fā)表于 04-26 10:10 ?498次閱讀
    JVM<b class='flag-5'>內存</b><b class='flag-5'>布局</b>詳解

    Rust的內部工作原理

    : google發(fā)布的 libtracecmd Rust wrapper 這個庫是libtracecmd的Rust wrapper,它允許編寫程序來分析由trace-cmd 生成的Linux的ftrace數據。 github地址
    的頭像 發(fā)表于 06-14 10:34 ?753次閱讀
    <b class='flag-5'>Rust</b>的內部工作原理

    稱重傳感器數量和量程原理

    稱重傳感器數量和量程原理
    的頭像 發(fā)表于 12-20 17:01 ?1452次閱讀
    <b class='flag-5'>淺</b><b class='flag-5'>聊</b>稱重傳感器數量和量程原理

    Rust開源社區(qū)推出龍架構原生適配版本

    Rust是近年來新興的系統(tǒng)級編程語言,專注于安全性、并發(fā)性和性能。Rust擁有豐富的類型系統(tǒng)和所有權模型,可通過在編譯時檢查內存訪問和并發(fā)問題,保證內存安全和線程安全。
    的頭像 發(fā)表于 07-17 16:54 ?462次閱讀
    <b class='flag-5'>Rust</b>開源社區(qū)推出龍架構原生適配版本