淺聊Rust程序內(nèi)存布局
內(nèi)存布局看似是底層和距離應(yīng)用程序開發(fā)比較遙遠的概念集合,但其對前端應(yīng)用的功能實現(xiàn)頗具現(xiàn)實意義。從WASM業(yè)務(wù)模塊至Nodejs N-API插件,無處不涉及到FFI跨語言互操作。甚至,做個文本數(shù)據(jù)的字符集轉(zhuǎn)換也得FFI調(diào)用操作系統(tǒng)鏈接庫libiconv,因為這意味著更小的.exe/.node發(fā)布文件。而C ABI與內(nèi)存布局正是跨(計算機)語言數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ)。
大約兩個月前,在封裝FFI閉包(不是函數(shù)指針)過程中,我重新梳理了Rust內(nèi)存布局知識點。然后,就有沖動寫這么一篇長文。今恰逢國慶八天長假,匯總我之所知與大家分享。開始正文...
存儲寬度size與對齊位數(shù)alignment— 內(nèi)存布局的核心參數(shù)
變量值在內(nèi)存中的存儲信息包含兩個重要屬性
首字節(jié)地址address
存儲寬度size
而這兩個值都不是在分配內(nèi)存那一刻的“即興選擇”。而是,遵循著一組規(guī)則:
address與size都必須是【對齊位數(shù)alignment】的自然數(shù)倍。比如說,
對齊位數(shù)alignment等于1字節(jié)的變量值可保存于任意內(nèi)存地址address上。
對齊位數(shù)alignment等于2字節(jié)且有效數(shù)據(jù)長度等于3字節(jié)的變量值
存儲寬度size等于0字節(jié)的變量值可接受任意正整數(shù)作為其對齊位數(shù)alignment— 慣例是1字節(jié)。
僅能保存于偶數(shù)位的內(nèi)存地址address上。
存儲寬度size也得是4字節(jié) — 從有效長度3字節(jié)到存儲寬度4字節(jié)的擴容過程被稱作“對齊”。
對齊位數(shù)alignment必須是2的自然數(shù)次冪。即,alignment = 2 ^ N且N是? 29的自然數(shù)。
存儲寬度size是有效數(shù)據(jù)長度加對齊填充位數(shù)的總和字節(jié)數(shù) — 這一點可能有點兒反直覺。
address,size與alignment的計量單位都是“字節(jié)”。
正是因為address,size與alignment之間存在倍數(shù)關(guān)系,所以程序?qū)?nèi)存空間的利用一定會出現(xiàn)冗余與浪費。這些被浪費掉的“邊角料”則被稱作【對齊填充alignment padding】。對齊填充的計量單位也是字節(jié)。根據(jù)“邊角料”出現(xiàn)的位置不同,對齊填充alignment padding又分為
小端填充Little-Endian padding—0填充位出現(xiàn)在有效數(shù)據(jù)右側(cè)的低位
大端填充Big-Endian padding—0填充位出現(xiàn)在有效數(shù)據(jù)左側(cè)的高位
文字抽象,圖直觀。一圖抵千詞,請看圖
延伸理解:借助于對齊位數(shù),物理上一維的線性內(nèi)存被重構(gòu)為了邏輯上N維的存儲空間。不嚴(yán)謹(jǐn)?shù)刂v,一個數(shù)據(jù)類型 ? 對應(yīng)一個對齊位數(shù)值 ? 按一個【單位一】將內(nèi)存空間均分一遍 ? 形成一個僅存儲該數(shù)據(jù)類型值(且只存在于算法與邏輯中)的維度空間。然后,在保存該數(shù)據(jù)類型的新值時,只要
選擇進入正確的維度空間
跳過已被占用的【單位一】(這些【單位一】是在哪一個維度空間被占用、是被誰占用和怎么占用并不重要)
尋找連續(xù)出現(xiàn)且數(shù)量足夠的【單位一】
就行了。
如果【對齊位數(shù)alignment】與【存儲寬度size】在編譯時已知,那么該類型就是【靜態(tài)分派】Fixed Sized Type。于是,
類型的對齊位數(shù)可由std::()讀取
類型的存儲寬度可由std::()讀取
若【對齊位數(shù)alignment】與【存儲寬度size】在運行時才可計算知曉,那么該類型就是【動態(tài)分派】Dynamic Sized Type。于是,
值的對齊位數(shù)可由std::(&T)讀取
值的存儲寬度可由std::(&T)讀取
存儲寬度size的對齊計算
若變量值的有效數(shù)據(jù)長度payload_size正好是該變量類型【對齊位數(shù)alignment】的自然數(shù)倍,那么該變量的【存儲寬度size】就是它的【有效數(shù)據(jù)長度payload_size】。即,size = payload_size;。 否則,變量的【存儲寬度size】就是既要大于等于【有效數(shù)據(jù)長度payload_size】,又是【對齊位數(shù)alignment】自然數(shù)倍的最小數(shù)值。
這個計算過程的偽碼描述是
variable.size = variable.payload_size.next_multiple_of(variable.alignment);
這個計算被稱作“(自然數(shù)倍)對齊”。
簡單內(nèi)存布局
基本數(shù)據(jù)類型
基本數(shù)據(jù)類型包括bool,u8,i8,u16,i16,u32,i32,u64,i64,u128,i128,usize,isize,f32,f64和char。它們的內(nèi)存布局在不同型號的設(shè)備上略有差異
在非x86設(shè)備上,存儲寬度size= 對齊位數(shù)alignment(即,倍數(shù)N = 1)
在x86設(shè)備上,因為設(shè)備允許的最大對齊位數(shù)不能超過4字節(jié),所以alignment ? 4 Byte
u64與f64的size = alignment * 2(即,N = 2)。
u128與i128的size = alignment * 4(即,N = 4)。
其它基本數(shù)據(jù)類型依舊size = alignment(即,倍數(shù)N = 1)。
FST瘦指針
瘦指針的內(nèi)存布局與usize類型是一致的。因此,在不同設(shè)備和不同架構(gòu)上,其性能表現(xiàn)略有不同
在非x86的
32位架構(gòu)上,size = alignment = 4 Byte(N = 1)
64位架構(gòu)上,size = alignment = 8 Byte(N = 1)
在x86的
size = 8 Byte
alignment = 4 Byte—x86設(shè)備最大對齊位數(shù)不能超過4字節(jié)
N = 2
32位架構(gòu)上,size = alignment = 4 Byte(N = 1)
64位設(shè)備上,
DST胖指針
胖指針的存儲寬度size是usize類型的兩倍,對齊位數(shù)卻與usize相同。就依賴于設(shè)備/架構(gòu)的性能表現(xiàn)而言,其與瘦指針行為一致:
在非x86的
size = 16 Byte
alignment = 8 Byte
N = 2
size = 8 Byte
alignment = 4 Byte
N = 2
32位架構(gòu)上,
64位架構(gòu)上,
在x86的
size = 16 Byte
alignment = 4 Byte—x86設(shè)備最大對齊位數(shù)不能超過4字節(jié)
N = 4
size = 8 Byte
alignment = 4 Byte
N = 2
32位架構(gòu)上,
64位設(shè)備上,
數(shù)組[T; N],切片[T]和str
str就是滿足UTF-8編碼規(guī)范的增強版[u8]切片。 存儲寬度size是全部元素存儲寬度之和
array.size = std::() * array.len(); 對齊位數(shù)alignment與單個元素的對齊位數(shù)一致。
array.alignment = std::();
()單位類型
存儲寬度size=0 Byte 對齊位數(shù)alignment=1 Byte 所有零寬度數(shù)據(jù)類型都是這樣的內(nèi)存布局配置。 來自【標(biāo)準(zhǔn)庫】的零寬度數(shù)據(jù)類型包括但不限于:
()單位類型 — 模擬“空”。
std::PhantomData— 繞過“泛型類型形參必須被使用”的編譯規(guī)則。進而,成就類型狀態(tài)設(shè)計模式中的Phantom Type。
std::PhantomPinned— 禁止變量值在內(nèi)存中“被挪來挪去”。進而,成就異步編程中的“自引用結(jié)構(gòu)體self-referential struct”。
自定義數(shù)據(jù)結(jié)構(gòu)的內(nèi)存布局
復(fù)合數(shù)據(jù)結(jié)構(gòu)的內(nèi)存布局描繪了該數(shù)據(jù)結(jié)構(gòu)(緊內(nèi)一層)字段的內(nèi)存位置“擺放”關(guān)系(比如,間隙與次序等)。在層疊嵌套的數(shù)據(jù)結(jié)構(gòu)中,內(nèi)存布局都是就某一層數(shù)據(jù)結(jié)構(gòu)而言的。它既承接不了來自外層父數(shù)據(jù)結(jié)構(gòu)的內(nèi)存布局,也決定不了更內(nèi)層子數(shù)據(jù)結(jié)構(gòu)的內(nèi)存布局,更代表不了整個數(shù)據(jù)結(jié)構(gòu)內(nèi)存布局的總覽。舉個例子,
#[repr(C)] struct Data { id: u32, name: String } #[repr(C)]僅只代表最外層結(jié)構(gòu)體Data的兩個字段id和name是按C內(nèi)存布局規(guī)格“擺放”在內(nèi)存中的。但,#[repr(C)]并不意味著整個數(shù)據(jù)結(jié)構(gòu)都是C內(nèi)存布局的,更改變不了name字段的String類型是Rust內(nèi)存布局的事實。若你的代碼意圖是定義完全C ABI的結(jié)構(gòu)體,那么【原始指針】才是該用的類型。
use ::{c_char, c_uint}; #[repr(C)] struct Data { id: c_uint, name: *const c_char // 注意對比 }
內(nèi)存布局核心參數(shù)
自定義數(shù)據(jù)結(jié)構(gòu)的內(nèi)存布局包含如下五個屬性
alignment
定義:數(shù)據(jù)結(jié)構(gòu)自身的對齊位數(shù)
規(guī)則:
alignment=2的n次冪(n是? 29的自然數(shù))
不同于基本數(shù)據(jù)類型alignment = size,自定義數(shù)據(jù)結(jié)構(gòu)alignment的算法隨不同的數(shù)據(jù)結(jié)構(gòu)而相異。
size
定義:數(shù)據(jù)結(jié)構(gòu)自身的寬度
規(guī)則:size必須是alignment自然數(shù)倍。若有效數(shù)據(jù)長度payload_size不足size,就添補空白【對齊填充位】湊足寬度。
field.alignment
定義:每個字段的對齊位數(shù)
規(guī)則:field.alignment=2的n次冪(n是? 29的自然數(shù))
field.size
定義:每個字段的寬度
規(guī)則:field.size必須是field.alignment自然數(shù)倍。若有效數(shù)據(jù)長度field.payload_size不足field.size,就添補空白【對齊填充位】湊足寬度。
field.offset
定義:每個字段首字節(jié)地址相對于上一層數(shù)據(jù)結(jié)構(gòu)首字節(jié)地址的偏移字節(jié)數(shù)
規(guī)則:
field.offset必須是field.alignment自然數(shù)倍。若不足,就墊入空白【對齊填充位】和向后推移當(dāng)前字段的起始位置。
前一個字段的field.offset + field.size ?后一個字段的field.offset
自定義枚舉類enum的內(nèi)存布局一般與枚舉類分辨因子discriminant的內(nèi)存布局一致。更復(fù)雜的情況,請見下文章節(jié)。
預(yù)置內(nèi)存布局方案
編譯器內(nèi)置了四款內(nèi)存布局方案,分別是
默認(rèn)Rust內(nèi)存布局 — 沒有元屬性注釋
C內(nèi)存布局#[repr(C)]
數(shù)字類型·內(nèi)存布局#[repr(u8 / u16 / u32 / u64 / u128 / usize / i8 / i16 / i32 / i64 / i128 / isize)]
僅適用于枚舉類。
支持與C內(nèi)存布局混搭使用。比如,#[repr(C, u8)]。
透明·內(nèi)存布局#[repr(transparent)]
僅適用于單字段數(shù)據(jù)結(jié)構(gòu)。
預(yù)置內(nèi)存布局方案對比
相較于C內(nèi)存布局,Rust內(nèi)存布局面向內(nèi)存空間利用率做了優(yōu)化— 省內(nèi)存。具體的技術(shù)手段包括Rust編譯器
重排了字段的存儲順序,以盡可能多地消減掉“邊角料”(對齊填充)占用的字節(jié)位數(shù)。于是,在源程序中字段聲明的詞法次序經(jīng)常不同于【運行時】它們在內(nèi)存里的實際存儲順序。
允許多個零寬度字段共用一個內(nèi)存地址。甚至,零寬度字段也被允許與普通(有數(shù)據(jù))字段共享內(nèi)存地址。
以C ABI中間格式為橋的C內(nèi)存布局雖然實現(xiàn)了Rust跨語言數(shù)據(jù)結(jié)構(gòu),但它卻更費內(nèi)存。這主要出于兩個方面原因:
C內(nèi)存布局未對字段存儲順序做優(yōu)化處理,所以字段在源碼中的詞法順序就是它們在內(nèi)存條里的存儲順序。于是,若 @程序員 沒有拿著算草紙和數(shù)著比特位“人肉地”優(yōu)化每個數(shù)據(jù)結(jié)構(gòu)定義,那么由對齊填充位冗余造成的內(nèi)存浪費不可避免。
C內(nèi)存布局不支持零寬度數(shù)據(jù)類型。零寬度數(shù)據(jù)類型是Rust語言設(shè)計的重要創(chuàng)新。相比之下,
(參見C17規(guī)范的第6.7.2.1節(jié))無字段結(jié)構(gòu)體會導(dǎo)致標(biāo)準(zhǔn)C程序出現(xiàn)U.B.,除非安裝與開啟GNU的C擴展。
Cpp編譯器會強制給無字段結(jié)構(gòu)體安排一個字節(jié)寬度,除非該數(shù)據(jù)結(jié)構(gòu)被顯式地標(biāo)記為[[no_unique_address]]。
以費內(nèi)存為代價,C內(nèi)存布局賦予Rust數(shù)據(jù)結(jié)構(gòu)的另一個“超能力”就是:“僅通過變換【指針類型】就可將內(nèi)存上的一段數(shù)據(jù)重新解讀為另一個數(shù)據(jù)類型的值”。比如,void * / std::c_void被允許指向任意數(shù)據(jù)類型的變量值例程。但在Rust內(nèi)存布局下,需要調(diào)用專門的標(biāo)準(zhǔn)庫函數(shù)std::transmute()才能達到相同的目的。 除了上述鮮明的差別之外,C與Rust內(nèi)存布局都允許【對齊位數(shù)alignment】參數(shù)被微調(diào),而不一定總是全部字段alignment中的最大值。這包括但不限于:
修飾符align(x)增加alignment至指定值。例如,#[repr(C, align(8))]將C內(nèi)存布局中的【對齊位數(shù)】上調(diào)至8字節(jié)
修飾符packed(x)減小alignment至指定值。例如,#[repr(packed)]將默認(rèn)Rust內(nèi)存布局中的【對齊位數(shù)】下調(diào)至1字節(jié)
結(jié)構(gòu)體struct的C內(nèi)存布局
結(jié)構(gòu)體算是最“中規(guī)中矩”的數(shù)據(jù)結(jié)構(gòu)。無論是否對結(jié)構(gòu)體的字段重新排序,只要將它們一個不落地鋪到內(nèi)存上就完成一多半功能了。所以,結(jié)構(gòu)體存儲寬度struct.size是全部字段size之和再(自然數(shù)倍)對齊于【結(jié)構(gòu)體對齊位數(shù)struct.alignment】的結(jié)果。有點抽象上偽碼
struct.size = struct.fields().map(|field| field.size).sum() // 第一步,求全部字段寬度值之和 .next_multiple_of(struct.alignment); // 第二步,求既大于等于【寬度值之和】,又是`struct.alignment`自然數(shù)倍的最小數(shù)值 相較于Rust內(nèi)存布局優(yōu)化算法的錯綜復(fù)雜,我好似只能講清楚C內(nèi)存布局的始末: 首先,結(jié)構(gòu)體自身的對齊位數(shù)struct.alignment就是全部字段對齊位數(shù)field.alignment中的最大值。
struct.alignment = struct.fields().map(|field| field.alignment).max(); 其次,聲明一個(可修改的)游標(biāo)變量offset_cursor以實時跟蹤(參照于結(jié)構(gòu)體首字節(jié)地址的)字節(jié)偏移量。游標(biāo)變量的初始值為0表示該游標(biāo)與結(jié)構(gòu)體的內(nèi)存起始位置重合。
let mut offset_cursor = 0; 接著,沿著源碼中字段的聲明次序,逐一處理各個字段:
【對齊】若游標(biāo)變量值offset_cursor不是當(dāng)前字段對齊位數(shù)field.alignment的自然數(shù)倍(即,未對齊),就計算既大于等于offset_cursor又是field.alignment自然數(shù)倍的最小數(shù)值。并將計算結(jié)果更新入游標(biāo)變量offset_cursor,以插入填充位對齊和向后推移字段在內(nèi)存中的”擺放“位置。
if offset_cursor.rem_euclid(field.alignment) > 0 { offset_cursor = offset_cursor.next_multiple_of(field.alignment); }
【定位】當(dāng)前游標(biāo)的位置就是該字段的首字節(jié)偏移量
field.offset = offset_cursor;
跳過當(dāng)前字段寬度field.size— 遞歸算法求值子數(shù)據(jù)結(jié)構(gòu)的存儲寬度。字段子數(shù)據(jù)結(jié)構(gòu)的內(nèi)存布局對上一層父數(shù)據(jù)結(jié)構(gòu)是黑盒的。
offset_cursor += field.size
繼續(xù)處理下一個字段。
然后,在結(jié)構(gòu)體內(nèi)全部字段都被如上處理之后,
【對齊】若游標(biāo)變量值offset_cursor不是結(jié)構(gòu)體對齊位數(shù)struct.alignment的自然數(shù)倍(即,未對齊),就計算既大于等于offset_cursor又是struct.alignment自然數(shù)倍的最小數(shù)值。并將計算結(jié)果更新入游標(biāo)變量offset_cursor,以增補填充位對齊和擴容有效數(shù)據(jù)長度至結(jié)構(gòu)體存儲寬度。
if offset_cursor.rem_euclid(struct.alignment) > 0 { offset_cursor = offset_cursor.next_multiple_of(struct.alignment); }
【定位】當(dāng)前游標(biāo)值就是整個結(jié)構(gòu)體的寬度(含全部對齊填充位)
struct.size = offset_cursor;
至此,結(jié)構(gòu)體的C內(nèi)存布局結(jié)束。然后,std::GlobalAlloc就能夠拿著這套“策劃案”向操作系統(tǒng)申請內(nèi)存空間去了。由此可見,每次【對齊】處理都會在有效數(shù)據(jù)周圍“埋入”大量空白“邊角料”(學(xué)名:對齊填充位alignment padding)。但出于歷史原因,為了完成與其它計算機語言的FFI互操作,這些浪費還是必須的。下面附以完整的偽碼輔助理解
// 1. 結(jié)構(gòu)體的【對齊位數(shù)】就是它的全部字段【對齊位數(shù)】中的最大值。 struct.alignment = struct.fields().map(|field| field.alignment).max(); // 2. 聲明一個游標(biāo)變量,以實時跟蹤(相對于結(jié)構(gòu)體首字節(jié)地址)的偏移量。 let mut offset_cursor = 0; // 3. 按照字段在源代碼中的詞法聲明次序,逐一遍歷每個字段。 for field in struct.fields_in_declaration_order() { if offset_cursor.rem_euclid(field.alignment) > 0 { // 4. 需要【對齊】當(dāng)前字段 offset_cursor = offset_cursor.next_multiple_of(field.alignment); } // 5. 【定位】字段的偏移量就是游標(biāo)變量的最新值。 field.offset = offset_cursor; // 6. 在跳過當(dāng)前字段寬度的字節(jié)長度(含對齊填充字節(jié)數(shù)) offset_cursor += field.size; } if offset_cursor.rem_euclid(struct.alignment) > 0 { // 7. 需要【對齊】結(jié)構(gòu)體自身 offset_cursor = offset_cursor.next_multiple_of(struct.alignment); } // 8. 【定位】結(jié)構(gòu)體的寬度(含對齊填充字節(jié)數(shù))就是游標(biāo)變量的最新值。 struct.size = offset_cursor;
聯(lián)合體union的C內(nèi)存布局
形象地講,聯(lián)合體是給內(nèi)存中同一段字節(jié)序列準(zhǔn)備了多套“數(shù)據(jù)視圖”,而每套“數(shù)據(jù)視圖”都嘗試將該段字節(jié)序列解釋為不同數(shù)據(jù)類型的值。所以,無論在聯(lián)合體內(nèi)聲明了幾個字段,都僅有一個字段值會被保存于物理存儲之上。從原則上講,聯(lián)合體union的內(nèi)存布局一定與占用內(nèi)存最多的字段一致,以確保任何字段值都能被容納。從實踐上講,有一些細節(jié)處理需要斟酌:
聯(lián)合體的對齊位數(shù)union.alignment等于全部字段對齊位數(shù)中的最大值(同結(jié)構(gòu)體)。
union.alignment = union.fields().map(|field| field.alignment).max();
聯(lián)合體的存儲寬度union.size是最長字段寬度值longest_field.size(自然數(shù)倍)對齊于聯(lián)合體自身對齊位數(shù)union.alignment的結(jié)果。有點抽象上偽碼
union.size = union.fields().map(|field| field.size).max() // 第一步,求最長字段的寬度值 .next_multiple_of(union.alignment); // 第二步,求既大于等于【最長字段寬度值】,又是`union.alignment`自然數(shù)倍的最小數(shù)值
舉個例子,聯(lián)合體Example0內(nèi)包含了u8與u16類型的兩個字段,那么Example0的內(nèi)存布局就一定與u16的內(nèi)存布局一致。再舉個例子,
use ::mem; #[repr(C)] union Example1 { f1: u16, f2: [u8; 4], } println!("alignment = {1}; size = {0}", mem::(), mem::()) 看答案之前,不防先心算一下,程序向標(biāo)準(zhǔn)輸出打印的結(jié)果是多少。演算過程如下:
字段f1的
存儲寬度size是2字節(jié)。
對齊位數(shù)alignment也是2字節(jié),因為基本數(shù)據(jù)類型的【對齊位數(shù)alignment】就是它的【存儲寬度size】。
字段f2的
存儲寬度size是4字節(jié),因為數(shù)組的【存儲寬度size】就是全部元素存儲寬度之和。
對齊位數(shù)alignment是1字節(jié),因為數(shù)組的【對齊位數(shù)alignment】就是元素的【對齊位數(shù)alignment】。
聯(lián)合體Example1的
對齊位數(shù)alignment就是2字節(jié),因為取最大值
存儲寬度size是4字節(jié),因為得取最大值
再來一個更復(fù)雜點兒的例子,
use ::mem; #[repr(C)] union Example2 { f1: u32, f2: [u16; 3], } println!("alignment = {1}; size = {0}", mem::(), mem::()) 同樣,在看答案之前,不防先心算一下,程序向標(biāo)準(zhǔn)輸出打印的結(jié)果是多少。演算過程如下:
字段f1的存儲寬度與對齊位數(shù)都是4字節(jié)。
字段f2的
對齊位數(shù)是2字節(jié)。
存儲寬度是6字節(jié)。
聯(lián)合體Example2的
對齊位數(shù)alignment是4字節(jié) — 取最大值,沒毛病。
存儲寬度size是8字節(jié),因為不僅得取最大值6字節(jié),還得向Example2.alignment自然數(shù)倍對齊。于是,才有了額外2字節(jié)的【對齊填充】和擴容【聯(lián)合體】有效長度6字節(jié)至存儲寬度8字節(jié)。你猜對了嗎?
不經(jīng)意的巧合
思維敏銳的讀者可以已經(jīng)注意到:單字段【結(jié)構(gòu)體】與單字段【聯(lián)合體】的內(nèi)存布局是相同的,因為數(shù)據(jù)結(jié)構(gòu)自身的內(nèi)存布局就是唯一字段的內(nèi)存布局。不信的話,執(zhí)行下面的例程試試
use ::mem; #[repr(C)] struct Example3 { f1: u16 } #[repr(C)] union Example4 { f1: u16 } // struct 內(nèi)存布局 等同于 union 的內(nèi)存布局 assert_eq!(mem::(), mem::()); assert_eq!(mem::(), mem::()); // struct 內(nèi)存布局 等同于 u16 的內(nèi)存布局 assert_eq!(mem::(), mem::()); assert_eq!(mem::(), mem::());
枚舉類enum的C內(nèi)存布局
突破“枚舉”字面含義的束縛,Rust的創(chuàng)新使Rust enum與傳統(tǒng)計算機語言中的同類項都不同。Rust枚舉類
既包括:C風(fēng)格的“輕裝”枚舉 — 僅標(biāo)記狀態(tài),卻不記錄細節(jié)數(shù)據(jù)。
也支持:Rust風(fēng)格的“重裝”枚舉 — 標(biāo)記狀態(tài)的同時也記錄細節(jié)數(shù)據(jù)。
在Rust References一書中,
“輕裝”枚舉被稱為“無字段·枚舉類 field-less enum”或“僅單位類型·枚舉類 unit-only enum”。
“重裝”枚舉被別名為“伴字段·枚舉類enum with fields”。
在Cpp程序中,需要借助【標(biāo)準(zhǔn)庫】的Tagged Union數(shù)據(jù)結(jié)構(gòu)才能模擬出同類的功能來。欲了解更多技術(shù)細節(jié),推薦讀我的另一篇文章。
禁忌:C內(nèi)存布局的枚舉類必須至少包含一個枚舉值。否則,編譯器就會報怨:error[E0084]: unsupported representation for zero-variant enum。
“輕裝”枚舉類的內(nèi)存布局
因為“輕裝”枚舉值的唯一有效數(shù)據(jù)就是“記錄了哪個枚舉項被選中的”分辨因子discriminant,所以枚舉類的內(nèi)存布局就是枚舉類【整數(shù)類型】分辨因子的內(nèi)存布局。即,
LightEnum.alignment = discriminant.alignment; // 對齊位數(shù) LightEnum.size = discriminant.size; // 存儲寬度 別慶幸!故事遠沒有看起來這么簡單,因為【整數(shù)類】是一組數(shù)字類型的總稱(餒餒的“集合名詞”)。所以,它包含但不限于
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 | 沒有概念對等項,可能得元編程了 | 等長于目標(biāo)架構(gòu)“瘦指針”寬度 |
維系FFI兩端Rust和C枚舉類分辨因子都采用相同的整數(shù)類型才是最“坑”的,因為
C / Cpp enum實例可存儲任意類型的整數(shù)值(比如,char,short,int和long)— 部分原因或許是C系語法靈活的定義形式:“typedef enum塊 + 具名常量”。所以,C / Cpp enum非常適合被做成“比特開關(guān)”。但在Rust程序中,就不得不引入外部軟件包bitflags了。
C內(nèi)存布局Rust枚舉類分辨因子discriminant只能是i32類型— 【存儲寬度size】是固定的4字節(jié)。
Rust內(nèi)存布局·枚舉類·分辨因子discriminant的整數(shù)類型是編譯時由rustc決定的,但最寬支持到isize類型。
這就對FFI - C端的程序設(shè)計提出了額外的限制條件:至少,由ABI接口導(dǎo)出的枚舉值得用int類型定義。否則,Rust端FFI函數(shù)調(diào)用就會觸發(fā)U.B.。FFI門檻稍有上升。 扼要歸納:
FFI - Rust端C內(nèi)存布局的枚舉類對FFI - C端枚舉值的【整數(shù)類型】提出了“確定性假設(shè)invariant”:枚舉值的整數(shù)類型是int且存儲寬度等于4字節(jié)。
C端 @程序員 必須硬編碼所有枚舉值的數(shù)據(jù)類型,以滿足該假設(shè)。
FFI跨語言互操作才能成功“落地”,而不是發(fā)生U.B.。
來自C端的遷就固然令人心情愉悅,但新應(yīng)用程序難免要對接兼容遺留系統(tǒng)與舊鏈接庫。此時,再給FFI - C端提要求就不那么現(xiàn)實了 — 深度改動“屎山”代碼風(fēng)險巨大,甚至你可能都沒有源碼。【數(shù)字類型·內(nèi)存布局】正是解決此棘手問題的技術(shù)方案:
以【元屬性】#[repr(整數(shù)類型名)]注釋枚舉類定義
明確指示Rust編譯器采用給定【整數(shù)類型】的內(nèi)存布局,組織【分辨因子discriminant】的數(shù)據(jù)存儲,而不總是遵循i32內(nèi)存布局。
從C / Cpp整數(shù)類型至Rust內(nèi)存布局元屬性的映射關(guān)系包括但不限于
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內(nèi)存布局的“輕裝”枚舉類Example5,因為它的每個枚舉值不是“無字段”,就是“單位類型”。于是,Example5的內(nèi)存布局就是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::()); 上面代碼定義的是【數(shù)字類型·內(nèi)存布局】的“輕裝”枚舉類Example6。它的內(nèi)存布局是u8類型的alignment = size = 1 Byte。
“重裝”枚舉類的內(nèi)存布局
【“重裝”枚舉類】絕對是Rust語言設(shè)計的一大創(chuàng)新,但同時也給FFI跨語言互操作帶來了嚴(yán)重挑戰(zhàn),因為在其它計算機語言中沒有概念對等的核心語言元素“接得住它”。對此,在做C內(nèi)存布局時,編譯器rustc會將【“重裝”枚舉類】“降維”成一個雙字段結(jié)構(gòu)體:
第一個字段是:剝?nèi)チ怂凶侄蔚摹尽拜p裝”枚舉】,也稱【分辨因子枚舉類Discriminant enum】。
第二個字段是:由枚舉值variant內(nèi)字段fields拼湊成的【結(jié)構(gòu)體struct】組成的【聯(lián)合體union】。
前者記錄選中項的“索引值” — 誰被選中;后者記憶選中項內(nèi)的值:根據(jù)索引值,以對應(yīng)的數(shù)據(jù)類型,讀/寫聯(lián)合體實例的字段值。 文字描述著實有些晦澀與抽象。邊看下圖,邊再體會。一圖抵千詞!(關(guān)鍵還是對union數(shù)據(jù)類型的理解)
上圖中有三個很細節(jié)的知識點容易被讀者略過,所以在這里特意強調(diào)一下:
保存枚舉值字段的結(jié)構(gòu)體struct A / B / C都既派生了trait Copy,又派生了trait Clone,因為
union數(shù)據(jù)結(jié)構(gòu)要求它的每個字段都是可復(fù)制的
同時,trait Copy又是trait Clone的subtrait
降維后結(jié)構(gòu)體struct Example7內(nèi)的字段名不重要,但字段排列次序很重要。因為在C ABI中,結(jié)構(gòu)體字段的存儲次序就是它們在源碼中的聲明次序,所以Cpp標(biāo)準(zhǔn)庫中的Tagged Union數(shù)據(jù)結(jié)構(gòu)總是,根據(jù)約定的字段次序,
將第一個字段解釋為“選中項的索引號”,
將第二個字段解讀為“選中項的數(shù)據(jù)值”。
C內(nèi)存布局的分辨因子枚舉類enum Discriminant的分辨因子discriminant依舊是i32類型值,所以FFI - C端的枚舉值仍舊被要求采用int整數(shù)類型。
舉個例子,
use ::mem; #[repr(C)] enum Example8 { Variant0(u8), Variant1, } println!("alignment = {1}; size = {0}", mem::(), mem::()) 看答案之前,不防先心算一下,程序向標(biāo)準(zhǔn)輸出打印的結(jié)果是多少。演算過程如下:
enum被“降維”成struct
就C內(nèi)存布局而言,struct的alignment是全部字段alignment中的最大值。
字段union.Variant0是單字段元組結(jié)構(gòu)體,且字段類型是基本數(shù)據(jù)類型。所以,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內(nèi)存布局的“輕裝”枚舉類。所以,tag.alignment = tag.size = 4 Byte
字段union是union數(shù)據(jù)結(jié)構(gòu)。所以,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內(nèi)存布局的“輕裝”枚舉類。所以,tag.size = 4 Byte
字段union是union數(shù)據(jù)結(jié)構(gòu)。union的size是全部字段size中的最大值。
于是,不精準(zhǔn)地struct.size ≈ 5 Byte(約等)
此刻struct.size并不是struct.alignment的自然數(shù)倍。所以,需要給struct增補“對齊填充位”和向struct.alignment自然數(shù)倍對齊
于是,struct.size = 8 Byte(直等)
哎!看見沒,C內(nèi)存布局還是比較費內(nèi)存的,一少半都是空白“邊角料”。 【“重裝”枚舉類】同樣會遇到FFI - ABI兩端【Rust枚舉類分辨因子discriminant】與【C枚舉值】整數(shù)類型一致約定的難點。為了遷就C端遺留系統(tǒng)和舊鏈接庫對枚舉值【整數(shù)類型】的選擇,Rust編譯器依舊選擇“降維”處理enum。但,這次不是將enum變形成struct,而是跳過struct封裝和直接以union為“話事人”。同時,將【分辨因子·枚舉值】作為union字段子數(shù)據(jù)結(jié)構(gòu)的首個字段:
對元組枚舉值,分辨因子就是子數(shù)據(jù)結(jié)構(gòu)第0個元素
對結(jié)構(gòu)體枚舉值,分辨因子就子數(shù)據(jù)結(jié)構(gòu)第一個字段。注:字段名不重要,字段次序更重要。
文字描述著實有些晦澀與抽象。邊看下圖,邊對比上圖,邊體會。一圖抵千詞!
由上圖可見,C與【數(shù)字類型】的混合內(nèi)存布局
既保證了降級后union與struct數(shù)據(jù)結(jié)構(gòu)繼續(xù)滿足C ABI的存儲格式要求。
又確保了【Rust端枚舉類分辨因子】與【C端枚舉值】之間整數(shù)類型的一致性。
舉個例子,假設(shè)目標(biāo)架構(gòu)是32位系統(tǒng),
use ::mem; #[repr(C, u16)] enum Example10 { Variant0(u8), Variant1, } println!("alignment = {1}; size = {0}", mem::(), mem::()) 看答案之前,不防先心算一下,程序向標(biāo)準(zhǔn)輸出打印的結(jié)果是多少。演算過程如下:
enum被“降維”成union
union的alignment是全部字段alignment中的最大值。
第一個字段是u16類型的分辨因子枚舉值。所以,Variant0.0.alignment = Variant0.0.size = 2 Byte
第二個字段是u8類型數(shù)字。所以,Variant0.1.alignment = Variant0.1.size = 1 Byte
于是,union.Variant0.alignment = 2 Byte
字段union.Variant0是雙字段元組結(jié)構(gòu)體。所以,struct的alignment是全部字段alignment中的最大值。
字段union.Variant1是單字段元組結(jié)構(gòu)體且唯一字段就是u16分辨因子枚舉值。所以,union.Variant1.alignment = union.Variant1.size = 2 Byte
于是,union.alignment = 2 Byte
union的size是全部字段size中的最大值。
第一個字段是u16類型的分辨因子枚舉值。所以,Variant0.0.size = 2 Byte
第二個字段是u8類型數(shù)字。所以,Variant0.1.size = 1 byte
于是,不精準(zhǔn)地union.Variant0.size ≈ 3 Byte(約等)
此刻union.Variant0.size不是union.Variant0.alignment的自然數(shù)倍。所以,需要對union.Variant0增補“對齊填充位”和向union.Variant0.alignment自然數(shù)倍對齊
于是,union.Variant0.size = 4 Byte(直等)
字段union.Variant0是雙字段元組結(jié)構(gòu)體。所以,struct的size是全部字段size之和。
字段union.Variant1是單字段元組結(jié)構(gòu)體且唯一字段就是u16分辨因子枚舉值。所以,union.Variant1.size = 2 Byte
于是,union.size = 4 Byte
哎!看見沒,C 內(nèi)存布局還是比較費內(nèi)存的,一少半的“邊角料”。
新設(shè)計方案好智慧
優(yōu)化掉了一層struct封裝。即,從enum ? struct ? union縮編至enum ? union
將被優(yōu)化掉的struct的職能(— 記錄選中項的“索引值”)合并入了union字段的子數(shù)據(jù)結(jié)構(gòu)中。于是,聯(lián)合體的每個字段
既要,保存枚舉值的字段數(shù)據(jù) — 舊職能
還要,記錄枚舉值的“索引號” — 新職能
但有趣的是,比較上一版數(shù)據(jù)存儲設(shè)計方案,C內(nèi)存布局卻沒有發(fā)生變化。邏輯描述精簡了但物理實質(zhì)未變,這太智慧了!因此,由Cpp標(biāo)準(zhǔn)庫提供的Tagged Union數(shù)據(jù)結(jié)構(gòu)依舊“接得住”Rust端【“重裝”枚舉值】。
僅【數(shù)字類型·內(nèi)存布局】的“重裝”枚舉類
若不以C加【數(shù)字類型】的混合內(nèi)存布局來組織枚舉類enum Example9的數(shù)據(jù)存儲,而僅保留【數(shù)字類型】內(nèi)存布局,那么上例中被降維后的【聯(lián)合體】與【結(jié)構(gòu)體】就都會缺省采用Rust內(nèi)存布局。參見下圖:
補充于最后,思維活躍的讀者這次千萬別想太多了。沒有#[repr(transparent, u16)]的內(nèi)存布局組合,因為【透明·內(nèi)存布局】向來都是“孤來孤往”的。
數(shù)字類型·內(nèi)存布局
僅【枚舉類】支持【數(shù)字類型·內(nèi)存布局】。而且,將無枚舉值的枚舉類注釋為【數(shù)字類型·內(nèi)存布局】會導(dǎo)致編譯失敗。舉個例子
#[repr(u16)] enum Example12 { // 沒有任何枚舉值 } 會導(dǎo)致編譯失敗error[E0084]: unsupported representation for zero-variant enum。
透明·內(nèi)存布局
“透明”不是指“沒有”,而是意味著:在層疊嵌套數(shù)據(jù)結(jié)構(gòu)中,外層數(shù)據(jù)結(jié)構(gòu)的【對齊位數(shù)】與【存儲寬度】等于(緊)內(nèi)層數(shù)據(jù)結(jié)構(gòu)的【對齊位數(shù)】和【存儲寬度】。因此,它僅適用于
單字段的結(jié)構(gòu)體 — 結(jié)構(gòu)體的【對齊位數(shù)】與【存儲寬度】等于唯一字段的【對齊位數(shù)】和【存儲寬度】。
struct.alignment = struct.field.alignment; struct.size = struct.field.size;
單枚舉值且單字段的“重裝”枚舉類 — 枚舉類的【對齊位數(shù)】與【存儲寬度】等于唯一枚舉值內(nèi)唯一字段的【對齊位數(shù)】和【存儲寬度】。
HeavyEnum.alignment = HeavyEnum::variant.field.alignment; HeavyEnum.size = HeavyEnum::variant.field.size;
單枚舉值的“輕裝”枚舉類 — 枚舉類的【對齊位數(shù)】與【存儲寬度】等于單位類型的【對齊位數(shù)】和【存儲寬度】。
LightEnum.alignment = 1; LightEnum.size = 0;
原則上,數(shù)據(jù)結(jié)構(gòu)中的唯一字段必須是非零寬度的。但是,若【透明·內(nèi)存布局】數(shù)據(jù)結(jié)構(gòu)涉及到了
類型狀態(tài)設(shè)計模式
異步多線程
,那么Rust內(nèi)存布局的靈活性也允許:結(jié)構(gòu)體和“重裝”枚舉值額外包含任意數(shù)量的零寬度字段。比如,
std::PhantomData為類型狀態(tài)設(shè)計模式,提供Phantom Type支持。
std::PhantomPinned為自引用數(shù)據(jù)結(jié)構(gòu),提供!Unpin支持。
舉個例子,
use ::{marker::PhantomData, mem}; #[repr(transparent)] enum Example13 { // 含`Phantom Type`的“重裝”枚舉類 Variant0 ( f32, // 普通有數(shù)據(jù)字段 PhantomData // 零寬度字段。泛型類型形參未落實到有效數(shù)據(jù)上。 ) } println!("alignment = {1}; size = {0}", mem::>(), mem::>()) 看答案之前,不防先心算一下,程序向標(biāo)準(zhǔn)輸出打印的結(jié)果是多少。演算過程如下:
因為Example14.Variant0.1字段是零寬度數(shù)據(jù)類型PhantomData,所以它的 和不參與內(nèi)存布局計算。
alignment = 1 Byte
size = 0 Byte
首字節(jié)地址address與Example10.Variant0.0字段重疊。
因為【透明·內(nèi)存布局】,所以 外層枚舉類的
【對齊位數(shù)】Example14.alignment = Example10::Variant0.0.alignment = 4 Byte
【存儲寬度】Example14.size = Example10::Variant0.0.size = 4 Byte
不同于【數(shù)字類型·內(nèi)存布局】,【透明·內(nèi)存布局】不被允許與其它內(nèi)存布局混合使用。比如,
#[repr(C, u16)]是合法的
#[repr(C, transparent)]和#[repr(transparent, u16)]就會導(dǎo)致語編譯失敗
其它類型的內(nèi)存布局
trait Object與由胖指針&dyn Trait/Box引用的變量值的【內(nèi)存布局】相同。
閉包Closure沒有固定的【內(nèi)存布局】。
微調(diào)內(nèi)存布局
只有Rust與C內(nèi)存布局具備微調(diào)能力,且只能修改【對齊位數(shù)alignment】參數(shù)值。另外,不同數(shù)據(jù)結(jié)構(gòu)可做的微調(diào)操作也略有不同:
struct,union,enum數(shù)據(jù)結(jié)構(gòu)可上調(diào)對齊位數(shù)
僅struct,union被允許下調(diào)對齊位數(shù)
數(shù)據(jù)結(jié)構(gòu)【對齊位數(shù)alignment】值的增加與減少需要使用不同的元屬性修飾符
#[repr(align(新·對齊位數(shù)))]增加對齊位數(shù)至新值。將小于等于數(shù)據(jù)結(jié)構(gòu)原本對齊位數(shù)的值輸入align(x)修飾符是無效的。
#[repr(packed(新·對齊位數(shù)))]減少對齊位數(shù)至新值。將大于等于數(shù)據(jù)結(jié)構(gòu)原本對齊位數(shù)的值輸入packed(x)修飾符也是無效的。
align(x)與packed(x)修飾符的實參是【目標(biāo)】字節(jié)數(shù),而不是【增量】字節(jié)數(shù)。所以,#[repr(align(8))]指示編譯器增加對齊數(shù)至8字節(jié),而不是增加8字節(jié)。另外,新對齊位數(shù)必須是2的自然數(shù)次冪。
禁忌
同一個數(shù)據(jù)類型不被允許既增加又減少對齊位數(shù)。即,align(x)與packed(x)修飾符不能共同注釋一個數(shù)據(jù)類型定義。
減小對齊位數(shù)的外層數(shù)據(jù)結(jié)構(gòu)禁止包含增加對齊位數(shù)的子數(shù)據(jù)結(jié)構(gòu)。即,#[repr(packed(x))]數(shù)據(jù)結(jié)構(gòu)不允許嵌套包含#[repr(align(y))]子數(shù)據(jù)結(jié)構(gòu)。
枚舉類內(nèi)存布局的微調(diào)
首先,枚舉類不允許下調(diào)對齊位數(shù)。 其次,上調(diào)枚舉類的對齊位數(shù)也會觸發(fā)“內(nèi)存布局重構(gòu)”的負作用。編譯器會效仿Newtypes 設(shè)計模式重構(gòu)#[repr(align(x))] enum枚舉類為嵌套包含了enum的#[repr(align(x))] struct元組結(jié)構(gòu)體。一圖抵千詞,請參閱下圖。
由上圖可見,在內(nèi)存布局重構(gòu)之后,C內(nèi)存布局繼續(xù)保留在枚舉類上,而align(16)修飾符僅對外層的結(jié)構(gòu)體有效。所以,從底層實現(xiàn)來講,枚舉類是不支持內(nèi)存布局微調(diào)的,僅能借助外層的Newtypes數(shù)據(jù)結(jié)構(gòu)間接限定。 以上面的數(shù)據(jù)結(jié)構(gòu)為例,
use ::mem; #[repr(C, align(16))] enum Example15 { A, B, C } println!("alignment = {1}; size = {0}", mem::(), mem::()) 看答案之前,不防先心算一下,程序向標(biāo)準(zhǔn)輸出打印的結(jié)果是多少。演算過程如下:
因為C內(nèi)存布局,所以枚舉類的分辨因子是i32類型和枚舉類的存儲寬度size = 4 Byte。
但,align(16)將內(nèi)存空間占用強制地從alignment = size = 4 Byte提升到alignment = size = 16 Byte。
結(jié)束語
這次分享的內(nèi)容比較多,感謝您耐心地讀到文章結(jié)束。文章中問答式例程的輸出結(jié)果,您猜對了幾個呀? 內(nèi)存布局是一個非常宏大技術(shù)主題,這篇文章僅是拋磚引玉,講的粒度比較粗,涉及的具體數(shù)據(jù)結(jié)構(gòu)也都很基礎(chǔ)。更多FFI和內(nèi)存布局的實踐經(jīng)驗沉淀與知識點匯總,我將在相關(guān)技術(shù)線的后續(xù)文章中陸續(xù)分享。
-
計算機
+關(guān)注
關(guān)注
19文章
7607瀏覽量
89821 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
3102瀏覽量
74883 -
程序
+關(guān)注
關(guān)注
117文章
3817瀏覽量
82164 -
函數(shù)指針
+關(guān)注
關(guān)注
2文章
57瀏覽量
3907 -
Rust
+關(guān)注
關(guān)注
1文章
233瀏覽量
6886
原文標(biāo)題:程序內(nèi)存布局
文章出處:【微信號:Rust語言中文社區(qū),微信公眾號:Rust語言中文社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
如何在Rust中使用Memcached
Rust GUI實踐之Rust-Qt模塊
Rust語言如何與 InfluxDB 集成
如何在Rust中讀寫文件
Rust 語言中的 RwLock內(nèi)部實現(xiàn)原理
淺談程序的內(nèi)存布局
【原創(chuàng)】聊一聊內(nèi)存指針操作
怎樣去使用Rust進行嵌入式編程呢
RUST在嵌入式開發(fā)中的應(yīng)用是什么
Rust原子類型和內(nèi)存排序
Rust語言助力Android內(nèi)存安全漏洞大幅減少
JVM內(nèi)存布局詳解

Rust的內(nèi)部工作原理

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

評論