記憶體管理

與 C/C++ 等程式語言不同,MicroPython 透過支援自動記憶體管理,將記憶體管理的細節對開發者隱藏起來。自動記憶體管理是作業系統或應用程式用來自動管理記憶體配置與釋放的一種技術。它能消除諸如忘記釋放配置給某物件之記憶體等難題,同時也避免了使用已釋放記憶體這個嚴重問題。自動記憶體管理有多種形式,垃圾回收(garbage collection,GC)便是其中之一。

垃圾回收器通常有兩項職責;

  1. 在可用記憶體中配置新物件。

  2. 釋放未使用的記憶體。

GC 演算法有很多種,但 MicroPython 採用 標記與清除 策略來管理記憶體。此演算法有一個標記階段,會遍歷堆積(heap)並標記所有存活的物件;清除階段則會走訪堆積,回收所有未被標記的物件。

MicroPython 的垃圾回收功能可透過 gc 內建模組取得:

>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>

即使呼叫了 gc.disable(),仍可使用 gc.collect() 觸發回收。

從 C 程式碼存取 MicroPython 記憶體

撰寫會從「Python 堆積」配置記憶體的 C 程式碼時(亦即 m_malloc()m_malloc0()m_free() 等函式),必須了解垃圾回收器的運作。

垃圾回收器的標記階段會從下列根(roots)開始,掃描指向堆積記憶體的存活指標:

  • 主 Python 執行階段(或 REPL)的堆疊。

  • 各個「Python 執行緒」的堆疊,適用於在原生作業系統執行緒或任務之上實作 Python 執行緒的移植版(port)。

  • 在 C 程式碼中使用巨集 MP_REGISTER_ROOT_POINTER 定義的「根指標」。這是讓指標以靜態範圍指向 Python 堆積的建議做法。

  • m_tracked_calloc()m_tracked_reallocm_tracked_free() 函式進行的受追蹤配置。這些特殊函式可配置一塊記憶體,且該記憶體始終被垃圾回收器視為存活。與 C 中的記憶體配置類似,這塊記憶體只能透過呼叫 m_tracked_free() 或軟重置(soft reset)來釋放。每次受追蹤配置都會帶來少量的記憶體用量與執行階段開銷。此功能並非在所有移植版上預設啟用。

接著垃圾回收器會遞迴掃描並標記根指標所指向的所有記憶體,直到處理完所有位址為止。這足以找出 MicroPython 執行階段仍在使用的所有 Python 物件。

然而,下列記憶體不會被垃圾回收器掃描,因而可能被過早釋放:

  • 含有指向堆積記憶體之指標的靜態或全域 C 變數。

  • 並非指向已配置緩衝區「開頭」(亦即 m_malloc() 所傳回的確切位址),而是指向已配置緩衝區內部某位址的指標(例如指向巢狀結構的指標)。基於效能考量,在這些情況下垃圾回收器不會標記其外層緩衝區。

  • 任何未執行 Python 程式碼、也未手動註冊為「Python 執行緒」的執行緒或 RTOS 任務的堆疊(適用於支援原生執行緒或任務的移植版)。

在這些情境中避免釋放後使用(use-after-free)的方法:

  • 使用受追蹤配置 API m_tracked_calloc()m_tracked_realloc()m_tracked_free()

  • 註冊一個根指標(見上文),而非將指標存放於靜態變數中。

  • 重新調整程式碼結構,例如提供一個 API,讓 Python 程式碼初始化一個單例 Python 物件(以 C 實作),由它持有所有相關指標,而不是將這些指標放在靜態變數中。

備註

軟重置 一律會清除 Python 堆積並釋放所有記憶體。軟重置後切勿持有任何指向堆積的指標,因為它們會變成指向已釋放記憶體的懸空指標。

某些移植版也支援「C 堆積」(見 C 動態記憶體配置),此時你可以透過呼叫標準 C 函式 malloc 等來配置在軟重置後仍保持有效的記憶體。

物件模型

所有 MicroPython 物件都以 mp_obj_t 資料型別來指稱。它通常是字組大小(亦即與目標架構上的指標相同大小),常見為 32 位元(STM32、RP2、nRF、Unix x86)或 64 位元(Unix x64)。對於某些物件表示法,它也可能大於一個字組,例如 OBJ_REPR_D 在 32 位元架構上具有 64 位元大小的 mp_obj_t

mp_obj_t 代表一個 MicroPython 物件,例如整數、浮點數、型別、dict 或類別實例。某些物件(如布林值與小整數)的值直接儲存在 mp_obj_t 值中,不需要額外記憶體。其他物件的值則儲存在記憶體中的其他位置(例如垃圾回收堆積上),而其 mp_obj_t 則含有指向該記憶體的指標。mp_obj_t 的一部分是標籤(tag),用來指明這是何種型別的物件。

可用表示法的具體細節請參閱 py/mpconfig.h

指標標記

由於指標是字組對齊的,當它們被儲存在 mp_obj_t 中時,此物件控制代碼的低位元會是零。例如在 32 位元架構上,最低的 2 個位元會是零:

********|********|********|******00

這些位元保留用於儲存標籤。標籤儲存額外資訊,這比在物件中新增一個欄位來儲存該資訊更有效率,後者可能效率不彰。在 MicroPython 中,標籤會指明我們處理的是小整數、駐留(interned)的(小)字串,還是具體物件,而每一種都適用不同的語意。

對於小整數,其對應方式如下:

********|********|********|*******1

其中星號保有實際的整數值。對於駐留字串或立即物件(例如 True),mp_obj_t 值的配置分別為:

********|********|********|*****010

********|********|********|*****110

而既非上述任何一種的具體物件則採取以下形式:

********|********|********|******00

此處的星號對應於該具體物件在記憶體中的位址。

物件的配置

小整數的值直接儲存在 mp_obj_t 中,並會就地(in-place)配置,而非配置在堆積或其他位置。因此,建立小整數不會影響堆積。對於文字資料已儲存於其他位置的駐留字串,以及 NoneFalseTrue 等立即值,情況亦同。

其餘所有具體物件都配置在堆積上,其物件結構會在物件標頭中保留一個欄位來儲存物件的型別。

+++++++++++
+         +
+ type    + object header
+         +
+++++++++++
+         + object items
+         +
+         +
+++++++++++

堆積最小的配置單位是區塊(block),其大小為四個機器字組(在 32 位元機器上為 16 位元組,在 64 位元機器上為 32 位元組)。另有一個同樣配置於堆積上的結構,用來追蹤每個區塊中物件的配置情形。此結構稱為點陣圖(bitmap)

../../_images/bitmap.png

點陣圖會追蹤區塊是「閒置」還是「使用中」,並以兩個位元來追蹤每個區塊的此狀態。

標記—清除垃圾回收器負責管理配置於堆積上的物件,同時也利用點陣圖來標記仍在使用中的物件。這些細節的完整實作請參閱 py/gc.c

配置:堆積配置(layout)

堆積的安排方式是由集區(pool)中的區塊所組成。一個區塊可以有不同的屬性:

  • ATB(配置表位元組,allocation table byte): 若設定,則該區塊為一般區塊

  • FREE: 閒置區塊

  • HEAD: 一連串區塊鏈的開頭

  • TAIL: 位於一連串區塊鏈的尾端

  • MARK: 已標記的開頭區塊

  • FTB(終結器表位元組,finaliser table byte): 若設定,則該區塊具有終結器(finaliser)