2.40. 記憶體與垃圾回收¶
MicroPython 管理記憶體的方式和 CPython 相同:每個物件都存活於 堆積(heap) 上,而 垃圾回收器(GC)會釋放不再被任何東西參考的物件。在一個只有幾百 KB RAM 的裝置上,留意堆積如何被使用偶爾是必要的;在桌面版 Python 上,則很少需要如此。
2.40.1. 堆積(heap)¶
堆積是執行階段配給 Python 物件的那一塊 RAM 區域 -- 在大多數 OpenMV 相機上它實際上橫跨不止一個實體記憶體池。每當一個 Python 運算式建立一個新物件(一個 list、一個字串、一個 dict、一個 tuple,任何不是小整數或單例的東西),就會從堆積中取出一塊位元組來容納它。當 GC 注意到某個物件已無法觸及時,就會把那塊位元組歸還給它原本所屬的可用池。
gc 模組中有兩個函式值得認識:
gc.mem_free()-- 此刻堆積上可用位元組的近似數量。gc.collect()-- 立即執行一次回收週期,而不必等待執行階段去觸發。
import gc
print("before:", gc.mem_free())
big = [0] * 10000
print("after :", gc.mem_free())
del big
gc.collect()
print("freed :", gc.mem_free())
確切的數字取決於版本;重要的是變化的 方向:配置大型物件會減少 mem_free,而捨棄這些參考再加上一次 gc.collect 則會把堆積歸還回來。
2.40.2. 碎片化¶
可用池並不會神奇地保持連續。隨著物件以不同的生命週期來來去去,可用空間即使總大小仍很大,也會碎裂成愈來愈小的區塊。配置一個比最大的單一可用區塊還大的物件會以 MemoryError 失敗 -- 即使技術上總可用 RAM 是足夠的。
同樣的總可用 RAM,能否容納一個大型緩衝區(左)或拒絕容納(右),取決於它碎片化的程度。¶
碎片化大多是長時間執行、不斷配置與釋放不同大小緩衝區的指令碼才需要擔心的問題。最有效的單一緩解措施,是在程式開頭附近、在大量短命的配置有機會把長命緩衝區打散之前,就先 預先配置 這些長命緩衝區。
2.40.3. 預先配置¶
有兩種模式能讓堆積行為良好:
把固定大小的緩衝區配置一次並重複使用,而不是每次迭代都建立一個新的 list 或
bytearray。把常數與查找表移出內層迴圈,讓它們只被建立一次。
buf = bytearray(64) # one allocation, reused below
def fill(value):
for i in range(len(buf)):
buf[i] = value
fill(0)
fill(255)
和一個在迴圈內建立新 bytearray 的版本相比:每次迭代都會產生 GC 之後得清理的垃圾。預先配置的版本則完全不產生垃圾。
2.40.4. 何時該呼叫 gc.collect¶
gc.collect() 通常是自動的 -- 當配置找不到足夠的可用記憶體時,執行階段就會觸發它。手動呼叫它在兩種情況下很有用:
在一大批物件離開作用域之後立即呼叫,以便馬上釋放它們,而不必等下一次配置去付出這個代價。
在一段需要已知最大可用記憶體量的區段之前立即呼叫,以避免 GC 在某個時間敏感的操作進行到一半時觸發。
到處灑滿 gc.collect 呼叫並不會讓程式更快 -- 回收本身就要花時間。請刻意地使用它,用在那些非預期回收的代價會比你刻意執行一次的代價更糟的時機點上。