Swift 與 Objective-C

[Swift 深入解析 #6] Swift 記憶體配置:屬性順序會改變大小

struct 大小不是屬性大小的簡單總和。本篇整理 size、stride、alignment、填充、利用 extra inhabitant 的 Optional,以及 64 位元 existential container 的實測。

閱讀 10 分鐘
[Swift 深入解析 #6] Swift 記憶體配置:屬性順序會改變大小 封面圖

在 Dispatch 篇看過「呼叫如何被翻譯」後,這次來看「值如何被配置」。

本文接續前一篇文章 Swift 深入解析 #5

struct 的大小是屬性大小的總和嗎?不是。

如果只改變屬性宣告順序,就可能改變 struct 大小,令人意外嗎?本篇是記憶體配置篇。

設計要放入陣列的數十萬個模型,或 Instruments 的記憶體圖比預期肥大時,這些知識就很重要。

也能回答 some·any 篇留下的問題:「那個盒子實際有多大?」

三種尺寸 — size、stride、alignment

Swift 標準提供查詢型別記憶體尺寸的工具:MemoryLayout

MemoryLayout<Int>.size       // 8
MemoryLayout<Bool>.size      // 1

struct Point { var x: Double; var y: Double }
MemoryLayout<Point>.size     // 16

尺寸有三種,意義各不相同。

size 是單一值的連續記憶體 footprint,不包含型別尾端的對齊填充。

alignment 是型別可放置位址的規則。CPU 從 8 的倍數位址讀取 8 位元組值通常更有效率,某些架構甚至要求如此。

因此每種型別都有「放在 n 的倍數位址」的限制。以下數值以目前 64 位元 Apple 平台為準:Double 的 alignment 是 8,Bool 是 1。

stride 是陣列中到下一個元素的距離。對齊限制可能在元素間產生填充,因此 stride ≥ size。

這是展示三者關係的經典範例。

struct Bad {
    var flag: Bool      // 1位元組
    var value: Double   // 8位元組 — 8的倍數位址
    var count: Int32    // 4位元組
}
MemoryLayout<Bad>.stride   // 24

struct Good {
    var value: Double   // 8
    var count: Int32    // 4
    var flag: Bool      // 1
}
MemoryLayout<Good>.stride  // 16

內容相同,只調整順序,陣列就能節省 33%。Bad 必須在 flag 後為 value 對齊而加入 7 位元組填充(Swift ABI Type Layout)。

相同三個盒子只改順序,24 位元組變成 16 位元組的對齊比較圖
內容相同,只改順序就能從 24 變成 16

先排列 alignment 較大的屬性,是減少填充的常見經驗法則。但不保證所有型別都最佳,最終結果應用 MemoryLayout 實測。

目前 Swift 的 fragile struct 配置演算法依宣告順序放置儲存屬性。但欄位順序不是語言或 ABI 的保證(Swift ABI · SE-0260)。需要精確 offset 的程式碼不可依賴觀察結果。

這項最佳化只有在實例很多時才有意義。設定物件多 8 位元組不重要,但 100 萬元素陣列每個多 8 位元組就是 8 MB(Swift ABI Type Layout)。

一如往常,先量測。

enum 的魔法 — Optional 標籤免費的時候

enum 配置展現 Swift 編譯器的巧思:必須儲存 case 標籤時,會把它藏進 payload 型別未使用的位元樣式(extra inhabitants)。

代表案例是 Bool?。目前 64 位元 Apple 平台上,MemoryLayout<Bool?>.size 是 1。

Bool 在 1 位元組中只使用 0、1 兩種樣式,因此可用剩餘樣式表示 nil。

參考型別 Optional 也相同。Swift 類別指標不把 null 當作物件,因此以 0 表示 nil。目前 64 位元下,AnyObject? 的 size 是 8。

但不是所有 Optional 都免費。Int 使用 8 位元組的所有位元樣式表示值,因此目前環境下 Int? 的 size 是 9、stride 是 16(Swift ABI)。

「安全幾乎免費」只適用於可重用多餘表示的型別。Optional 語法本身不保證所有型別都零記憶體成本。

實測盒子 — 單一協定 existential 是 5 個 word

some·any 篇只說過「any 是有成本的盒子」,現在可以實測。

protocol Shape {}
MemoryLayout<any Shape>.size   // 40 — 目前 64位元環境

目前 64 位元環境中,單一非類別限制協定 any Shape 是 40 位元組,也就是 5 個 word:3 個 word 的值緩衝區、型別 metadata 指標,以及一個 witness table 指標。

關鍵在於值的 size 與 alignment 是否都符合 3 個 word 的 inline 緩衝區條件。

超過條件時,值會配置到獨立儲存空間,緩衝區只放指標。此時可能增加 heap 配置、參考計數與間接存取成本(Swift ABI · SE-0335)。

單一協定 existential 的 3-word 值緩衝區,以及 metadata、witness table 剖面圖
64 位元單一非類別協定 existential 是 5 個 word,超過緩衝區條件的值會使用獨立儲存空間

5 個 word 不是所有 any 型別的固定大小。協定組合可能增加 witness table 指標。

僅限類別的協定 existential 使用更小的配置,不需要值緩衝區與額外 metadata 指標。

若協定導向程式碼比預期慢,可考慮用 some·泛型移除盒子,或縮小承載型別。實際效果必須用 Instruments 與基準測試確認(SE-0335 Existential any)。

目前 64 位元 Apple 平台上,類別型別值的 stride 是 8,代表參考大小,不是物件大小。

原生 Swift heap 物件通常有包含 metadata 指標與 inline 參考計數資訊的標頭;64 位元下為 16 位元組。實際配置可能因屬性對齊與 allocator 捨入而更大(Swift HeapObject · Swift RefCount)。

ARC 篇提到的參考型別隱藏成本,就是這個標頭及配置、釋放、計數操作。若把數十萬個小資料片段做成類別,管理成本可能高於資料本身。

實務工具箱 — 重新量測、確認、保持懷疑

整理處理這一層的實務工具。

**用 MemoryLayout 驗證假設。**設計大量產生的模型 struct 時,養成量一次 stride 的習慣即可。

也可在單元測試中加入如 #expect(MemoryLayout<Tick>.stride <= 32) 的預算,日後增加屬性超出預算時會立即顯現。

**用 Instruments Allocations 檢查 heap。**若「明明以 struct 為主卻有很多 heap 配置」,嫌疑對象很明確。

any 盒子、閉包捕獲、CoW(copy-on-write)儲存區,以及類別實例。Allocations 的型別統計會指出是哪一項。

**copy-on-write 集合的錯覺:**目前 64 位元 Apple 平台上,MemoryLayout<[Int]>.stride 是 8。Array 值是間接參考 heap 儲存區的小型 struct,但不可將此數值視為固定 ABI。

這就是不能因為含陣列的 struct stride 很小,就判定它「很輕」的原因。

真正的重量在 heap 儲存區,由 Allocations 顯示。CoW 原理如另一篇文章所述。

最後談平衡感:這一篇的知識並非每天都會用到。

但知道「為什麼」的價值會在問題發生時顯現。記憶體圖異常時,可以從嫌疑清單而非猜測開始,這就是深入一層的優勢。

總結

  • 型別有三種尺寸:size 是不含 tail padding 的 footprint,alignment 是位址對齊規則,stride 是連續配置間距。大量產生的 struct 常可藉由集中高 alignment 屬性減少填充,但必須實測。
  • enum 可將標籤藏進 extra inhabitant。Bool? 與參考型別 Optional 不需額外空間,但 Int? 等型別需要獨立標籤空間。
  • 64 位元單一非類別協定 existential 是 5 個 word。值的 size 或 alignment 超過 3-word 緩衝區條件時,可能產生獨立儲存與間接成本。
  • 目前 64 位元原生 Swift 物件的基本標頭通常是 16 位元組。實際配置可能因屬性與 allocator 對齊而更大。
  • 工具是 MemoryLayout(設計驗證)與 Instruments Allocations(實測)。沒有量測的配置最佳化就是過早最佳化。

下一篇討論編譯時的魔法:在 #Preview 與 @Observable 背後讓程式碼撰寫程式碼的 Swift 巨集。

來源與確認標準

  • MemoryLayout — Apple Developer Documentation · 確認 2026-08-19 · 依據:size、alignment、stride 的 API 定義
  • Swift ABI Type Layout — Swift 專案 · 官方文件 · 確認 2026-08-19 · 依據:struct、enum、existential container 配置
  • SE-0260 Library Evolution — Swift Evolution · 確認 2026-08-19 · 依據:struct 欄位順序的語言與 ABI 保證範圍
  • SE-0335 Existential any — Swift Evolution · 確認 2026-08-19 · 依據:existential 的 3-word inline 緩衝區與動態記憶體成本
  • Swift HeapObject · Swift RefCount — Swift Runtime · 確認 2026-08-19 · 依據:原生 heap 物件標頭與 inline 參考計數

延伸閱讀

Swift 深入解析系列