Rust 所有權 (1) - 記憶體架構、heap、stack
Table of Contents
前言
上一篇中在猜數字遊戲中看到了這段程式碼:
其中我好奇這個 &mut
的語法如果不寫會怎麼樣?來試著拿掉後編譯看看,會發現印出這樣的錯誤:
這個錯誤會說需要使用 &mut
這樣的 mutably borrowing
的型別,那這個 borrow 又是在借什麼呢?
這就會跟 Rust 其中一個重要的特性 —— 所有權 (Ownership) 有關,而這對像我這樣平常大多寫動態語言的人來說,可能就是學習 Rust 的第一道坎,那以下就跟著我這個 Rust 新手一起來由淺入深吧。
程式管理記憶體的方式
參考文件定義,所有權是指 Rust 中用來管理程式記憶體的一系列規則。在了解是什麼規則前,會需要先學習「程式是如何管理記憶體的」這件事。
參考這個影片中提到的,程式管理記憶體的方式有三種:
- GC (Garbage collection)
- 主要被用在各種高階語言中,像是 Java、Python、JavaScript、Go、C# 等
- 優點
- 大多情況不會遇到記憶體處理的錯誤,但開發者仍需避免一些該語言中可能造成記憶體使用不當的寫法。像以 JS 而言,沒適時地清掉事件監聽器、
clearTimeout
等,久而久之可能會造成記憶體洩漏 (memory leaks) 讓程式卡頓或 crash 掉 - 因為記憶體管理直接交由每個語言的 GC 去自動處理,學習曲線低、開發起來較快
- 大多情況不會遇到記憶體處理的錯誤,但開發者仍需避免一些該語言中可能造成記憶體使用不當的寫法。像以 JS 而言,沒適時地清掉事件監聽器、
- 缺點
- 沒辦法控制記憶體而造成效能較慢
- GC 在清理記憶體時可能造成卡頓
- 手動管理記憶體
- 像是 C 語言可以使用
malloc
(記憶體配置 memory allocation 的縮寫)、free
,C++ 可以使用new
、delete
來操作記憶體的配置與釋放 - 優點:更快的效能,且比起用 GC 的語言有較小的程式大小
- 缺點:學習曲線高且不好寫,沒寫好的話在執行過程中可能會遇到各種記憶體錯誤
- 像是 C 語言可以使用
- 所有權系統
- Rust 用來確保 memory safety 的記憶體管理系統
- 優點:結合前述 C、C++ 的優點之外,還能在編譯時就提示錯誤,避免在執行時遇到記憶體錯誤
- 缺點:學習曲線高
Memory 中的 Stack 與 Heap
記憶體架構
而要了解記憶體管理,另一個重點就是要知道一段執行中的程式的記憶體架構是什麼,這裡參考幾個教學影片與文章整理出上面這張圖,有興趣也可以參考下方的延伸閱讀,首推這個影片與這篇文章。
下面也大概說明一下記憶體架構,由低位到高位可以分成 4 個區段:
- Machine code:就是編譯後的讓電腦能讀懂的機器碼,有的影片或文章會直接寫成 Code 或是 Text 來表示
- Static:程式碼中宣告的常數與全域變數 (global variables)
- Stack
- 中文翻作記憶體堆疊,但還是直稱 memory stack 比較能理解
- 跟資料結構的 stack 不是同個東西,但有沿用其後進先出 (LIFO) 的特性
- 由各個編譯器在編譯程式時決定一段連續固定大小的記憶體
- 能存以下資訊:
- 函式中的 local 變數
- 函式間呼叫的 call stack 資訊。因此當有太深層的遞迴函式呼叫時會造成所謂的 stack overflow 問題
- heap 記憶體位址,也就是 C/C++ 中的指標或我們常聽到的 reference
- Heap
- 中文翻作記憶體堆積,但堆疊跟堆積常常會搞混,覺得還是保留原文更好記
- 跟資料結構的 heap 不是同個東西概念也完全不同。資料結構中的 heap 是一個樹狀結構,但記憶體中的 memory heap 指得是一個大型的記憶體池子,裡面有使用中的記憶體、閒置的記憶體
- 主要用來存動態大小或未知大小的資料,像是配置一段空間給陣列使用
- 如果使用的程式語言沒有適當地去釋放記憶體或自動做 GC,隨著可用的記憶體逐漸減少,最終會造成卡頓或 crash,也就是所謂的 memory leaks
Stack 與 Heap 的關係
而 stack 與 heap 的關係,可以直接參考這個影片中的上面這張截圖,雖然範例是 C 語言,但程式碼算蠻單純的應該不會太難懂:
- 宣告一個 local 變數
a
,會被放進 stack 中 - 宣告一個
p
,先到 heap 中配置一段可以存整數大小的記憶體區段,並取得這個區段的記憶體位址 (例如圖上的200
),存到 stack 中,而這個p
也就是 C 語言中指標的概念,這個變數會指向 heap 中實際存放的 value
假如今天在 C 中沒去用 free
做記憶體釋放,就把 p
指向另一段新的記憶體位址時 (例如圖上的 400
),這個 200
區段的記憶體很可能造成浪費。而如果是像前面提到的有 garbage collector 的語言就會定時把這些沒用到的記憶體清掉。
而在 Rust 裡面沒有 garbage collector,它又是如何優雅地管理記憶體的呢,且待下回分解。
延伸閱讀
- 關於記憶體架構、stack、heap
- Rust 所有權觀念