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,不包含型別末端為對齊而產生的 tail padding。

**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 篇所說的「安全性幾乎免費」,對能重用多餘表示的型別而言是準確的。Optional 語法本身不保證所有型別的記憶體成本為零。

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

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

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

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

關鍵分歧在於所容納值的 size 與 alignment 是否都符合 3 個 word 的內嵌緩衝區條件。

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

單一協定 existential 的 3 個 word 值緩衝區,以及中繼資料與 witness table 的剖面圖
64 位元單一非類別協定 existential 為 5 個 word;超過緩衝區條件的值會使用獨立儲存空間

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

類別專用協定 existential 使用更小的配置,不需要值緩衝區和額外的中繼資料指標。

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

也來看看類別執行個體的尺寸。目前 64 位元 Apple 平台上,類別型別值的 stride 是 8,代表參考的大小,不是物件大小。

原生 Swift 堆積物件通常有包含中繼資料指標和內嵌參考計數資訊的標頭,64 位元下為 16 位元組。實際配置大小可能因屬性對齊和配置器捨入而更大(Swift HeapObject · Swift RefCount)。

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

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

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

**用 MemoryLayout 驗證假設。**設計大量產生的模型 struct 時,養成測量一次 stride 的習慣就很有幫助。

也可以像 #expect(MemoryLayout<Tick>.stride <= 32) 一樣在單元測試中加入預算。之後新增屬性超過預算時,就會立即發現。

**用 Instruments Allocations 檢查堆積。**如果明明以 struct 為主卻有大量堆積配置,嫌疑對象很明確。

any 盒子、閉包捕獲(閉包篇)、CoW(copy-on-write)儲存,以及類別執行個體。Allocations 的依型別統計會告訴你是哪一類。

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

這就是為什麼含有陣列的 struct stride 很小,也不能下結論說它「很輕」。

真正的重量在堆積儲存中,而 Allocations 會顯示它。CoW 原理已在另一篇文章中說明。

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

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

總結

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

下一篇討論編譯期魔法:Swift 巨集,也就是 #Preview 和 @Observable 背後讓程式碼撰寫程式碼的機制。

來源與確認依據

  • 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 內嵌緩衝區與動態記憶體成本
  • Swift HeapObject · Swift RefCount — Swift Runtime · 確認 2026-08-19 · 依據:原生堆積物件標頭與內嵌參考計數

延伸閱讀