CPython和MicroPython中的內存使用
Python部落(python.freelycode.com)組織翻譯,禁止轉載,歡迎轉發。
在PyCon 2017上,Kavya Joshi關注了Python引用的實現在CPython和MicroPython上的不同。她特別描述了二者在內存使用和處理上的區別。這些不同是使MicroPython能運行在內存嚴重受限的微控制器上的原因之一,而CPython很難在這種環境上運行。
她在演講中提到CPython是標準和默認的實現,大家都喜歡並一直在使用它。但在內存使用方面,CPython的聲譽並不怎麼樣。這催生了一些替代方案的產生,MicroPython正是其中之一。
作為「最小最嚴苛」的替代方案,MicroPython以微控制器作為目標,從而使得Python可以用來控制硬體。MicroPython可以運行在16KB RAM和256KB ROM的設備上;它實現了Python3.4絕大部分的功能。由於諸如元類、多進程這樣的內容在微控制器上沒有多大意義,所以語言和標準庫中的這一部分被刪除了。
Joshi說道,CPython和MicroPython在後台頗為相似。它們都是基於堆棧的虛擬機的位元組碼解釋器。它們都是C程序。然而,在內存使用方面,它們卻截然相反。
作為一個簡單的試驗,沒有任何形式的基準,她測試了Python3.6和MicroPython1.8在一台4GB運存的系統為64位Ubuntu 16.10的電腦上的表現。測試內容是創建20萬個不同類型的對象(整數,字元型,列表)並保證它們不被回收。然後,她從Python本身中測量了程序的堆使用情況。
CPython和MicroPython都使用自定義分配器管理堆。但CPython根據需要增加堆,而MicroPython則使用一個固定大小的堆。由於Joshi測量的是堆的使用情況,這個固定大小也就是測量值的上限。但是,正如我們所見,MicroPython和CPython使用堆的方式並不一樣,這也會影響到測量值。
從整數測試開始,她展示了堆使用情況的圖表(Joshi的幻燈片在這裡Speaker Deck)。該測試為從10的10次方(在Python中表示為10 ** 10)開始的20萬個連續整數創造了整型對象。CPython的圖表顯示堆使用量在線性增長,而MicroPython則完全是一條直線。字元對象的測試也是相同的結果。CPython的堆使用比MicroPython更多,但實際數字並不那麼重要,真正讓她感興趣的是圖形的形狀。
但是,列表測試的結果則略有不同。該測試創建了一個列表,然後持續將很小的整型添加到列表中;結果圖表顯示堆使用和列表一樣越來越大。CPython和MicroPython的結果圖都顯示了堆使用量的階梯函數,但MicroPython每一步的高度和深度看上去都是CPython的兩倍,而CPython每一步的增加則顯得平緩一些。她想回答的問題之一是為什麼這兩個解釋器在內存足跡和內存剖面上有如此大的不同。
對象和內存
為了解釋到底發生了什麼,她需要展示兩個解釋器內部是如何實現對象的。既然CPython在測試中使用了更多的內存,一定有內部的原因。不是CPython的對象要大一些就是CPython分配了超過它們大小的內存-也許二者兼有。
CPython把所有對象都分配在堆上,所以語句「x = 1」將會為「1」在堆上創建一個對象。每個對象包含兩個部分,頭部(PyObject_HEAD)和一些可變對象的特定欄位。頭部是對象的開銷,包括兩個8位元組欄位,一個是引用計數器,一個是指向對象類型的指針。這意味著16位元組是一個CPython對象大小的下限。
對一個整型對象來說,它的對象特定區域包括一個8位元組長的標誌記錄後面有多少個四位元組值(記住Python可以代表任意大小的整數)。所以存儲一個值小於230-1的整數對象(有兩個位元組用於其他用途)需要28位元組,其中有24個位元組是額外的開銷。而對測試中的值(1010以上),每個對象將佔用32個位元組,故20萬個整數對象將要消耗6MB的內存。
而在MicroPython中,每個對象只有8位元組。這八個位元組既可以直接使用,也可以作為 指向其他數據結構的指針。指針標註允許八位元組對象有多個用途。它被用來編碼對象的一些額外信息;由於所有的內存地址都是八位元組邊界的別名,三個低位位元組可以用來存儲標籤值。但並不是所有的標籤位都是這樣使用的;如果低序列位是一個,表明是存儲在另外63位的小整數。一個102在低序的兩位代表剩餘數之中的62位指向一個網路字元串(interned string)。002在這些位上表明其他位是指向具體對象的指針(即一些既不是小整型或者網路字元串的對象)。
由於測試中儲存的值將會以小整數來對待,因此每個對象只佔用8位元組。這些都存儲在棧上而不是堆,這也就解釋了為什麼MicroPython的測試結果是一條水平線。Joshi說道,即使你去測試棧的使用情況(可能棧的測量會有點複雜),MicroPython也要比CPython的內存使用少得多。
對於創建20萬個長度小於10的字元串對象的測試來說,原理也是相似的。CPython中ASCII字元串對象(PyASCIIObject)使用48位元組的頭部,所以一個長度為1兩個位元組的字元串(如「a」加一個空白終止符)佔用50個位元組;一個長度為10的字元串將佔用59個位元組。MicroPython將小的字元串(長度小於254)存儲為使用3位元組頭部的數組。所以長度為1的字元串佔用5位元組,只相當於CPython中需要空間的十分之一。
這仍然不足以解釋結果上呈現的水平線。她說道,MicroPython中的字元串存儲為一個預分配的數組。因此當字元串對象創建時並沒有新堆進行分配。實際上,分配總是及時移除的,因此在圖像上看不到堆的增加。
可變對象
字元串和證書都是不可變對象,但第三項測試使用了可變對象--列表。在CPython中,可變對象以PyGC_HEAD結構體增加了頭部。這個結構用來進行垃圾回收跟蹤,佔用24個位元組。總的來說,一個PyListObject長度為64位元組;但你同樣需要為列表中的每個對象添加指針,這些指針存儲在一個數組中,並分配每個對象的存儲空間。
列表測試結果圖中的階梯表明列表元素的數組動態調整大小的變化。如果每次追加操作都會進行大小的調整,結果圖將會和整型、字元型的線性遞增變化類似,但CPython提前為後期的追加操作分配了內存。這樣的機制稀釋了多重拼接操作的內存分配的開銷。
MicroPython的垃圾回收機制和CPython完全不同,它並沒有使用引用計數。所以,MicroPython中用於列表的具體的對象並沒有引用計數;但它也和CPython一樣使用了類型指針。和CPython相比,移除引用計數器節約了八位元組的內存。另外,MicroPython也沒有為垃圾回收使用額外的頭部,節約了24位元組。這樣,與CPython相比,MicroPython中的可變對象共節約了32位元組。
所以結果就是CPython對象要比MicroPython大很多。其他對象類型,尤其是類,在和CPython3.6比起來就沒有這麼明顯的差距了,Joshi補充道,因為新版本的CPython進行了優化,大大減少了這方面的開銷。除此之外,CPython在測試中比MicroPython分配了更多的對象。MicroPython直接把整型對象存儲在棧上,而不像CPython那樣將其分配在堆上。
垃圾回收
頭部如何跟蹤CPython對象的垃圾回收可能是觀眾們最不容易理解的一部分,Joshi說。CPython使用引用計數器,可以很容易得追溯一個對象的引用;每當一個對象被複制,或者放入列表,插入一個字典等等,它的計數器都會增加。而當一個引用消失,例如使用del操作或者一個變數超出範圍,計數器相應得減少。而當計數器為0時,對象將會被回收。
但也並不是所有情況都是這樣的,這是因為PyGC_HEAD結構體在起作用。如果一個對象引用自它們本身或者間接包含對自身的引用,會構成循環引用。她舉了個例子:
如果x隨後超出範圍或者執行了del(x)操作,引用計數器並不會變為0,而對象也永遠不會被回收。CPython使用一個循環垃圾回收器來檢測中斷循環引用,解決這種問題。由於只有可變對象可能會有這樣的循環引用,它們是唯一使用PyGC_HEAD信息來進行跟蹤的對象。
MicroPython使用點陣圖追蹤堆的分配。它將堆分解成32位元組的分配單元,每個單元使用兩位來記錄回收或者使用中的追蹤信息。通過使用一個記錄、掃描垃圾回收器在定期運行中維護點陣圖;它管理了所有分配在堆上的對象。通過這種方式,標記掃描方法有效得以執行時間換取了跟蹤內存信息開銷的減少。
Joshi說她僅僅用了很膚淺的優化就減少了MicroPython的內存使用。但 代碼是可用的; 它很容易理解,感興趣的話大家都可以看看。
不同的方法
在分享的結尾,她總結了為什麼CPython和MicroPython選擇各自的方法。一般情況下,我們總要在內存使用和性能之間做出權衡,但本文的例子並非如此。MicroPython在大部分的性能測試中都優於CPython, Joshi說,儘管它確實可能在一些性能測試中表現的更差尤其是一些包含巨大字典的場景中。這裡的權衡是功能性上的,MicroPython並沒有實現CPython的完整特性。這意味著,例如MicroPython的使用者無法像CPython的開發者那樣能享受第三方庫的完整生態。
CPython和MicroPython項目做出的不同設計決策有一部分原因是其發展背後的哲學。回到CPython剛被發明的20世紀八十年代末九十年代初,開發者當時想要的是「簡單的實現和設計優先」,他們將會在後面考慮性能的問題。
這和MicroPython背後的哲學恰恰相反。它是一個比較新的實現,所以可以從CPython產生後的十多年發展出來的諸如Lua、JavaScript或其他語言借鑒一些新想法。MicroPython也僅僅針對這些高度受限的微控制器來服務。事實上,這些微控制器的目標是很多不同的系統和使用場景,這也可以幫助解釋其做選擇的原因。
英文原文:https://lwn.net/Articles/725508/
譯者:mrwoody
※Ubuntu可以從Windows商店下載使用了
※尾遞歸——寫給命令式編程程序員
※Python 3.6 為賬號和密碼安全添加了新的 secrets 模塊
※秀好評:不秀白不秀
※outfancy-終端列印表格的利器
TAG:Python部落 |
※LeakCanary Android中內存泄露
※Facebook開源Mask R-CNN的PyTorch 1.0基準,比mmdetection更快、更省內存
※業界 | Facebook開源Mask R-CNN的PyTorch 1.0基準,比mmdetection更快、更省內存
※Microsoft Exchange Server 內存破壞漏洞
※Intel Lakefield SoC將直接整合內存,由Foveros 3D工藝打造
※你的手機內存只有4G?Sorry,它叫iPhone XS Max
※Facebook開源Mask R-CNN的PyTorch 1.0基準,更快、更省內存
※首款4GB運行內存的iPhone,是iPhoneSE二代嗎?
※C++重載 operator new 和 operator delete 實現內存泄漏跟蹤器
※Spark 源碼分析之ShuffleMapTask內存數據Spill和合併
※Intel 10nm Sunny Cove新架構探秘:內存變革
※深入研究EF Core AddDbContext 引起的內存泄露的原因
※HyperX Predator DDR4 RGB內存條:華麗登場!
※AMD處理器的內存超頻世界紀錄被刷新:用的是Ryzen 5 3600X+Crosshair VIII Impact
※全球最大 Integral Memory推512GB內存卡
※vivo新機亮相GeekBench:只有2GB內存
※Integral Memory的512GB內存卡,擁有世界最大容量
※疑似iPad Air和iPad mini 5跑分出爐 運行內存良心!
※淺談C++ allocator內存管理(對比new的局限性)
※「iPhone X Plus」將搭載4GB內存,比iPhone X提高了18%