MicroPython 字串駐留(string interning)¶
MicroPython 使用 string interning 來同時節省 RAM 與 ROM。這可避免儲存同一字串的重複副本。這主要適用於程式碼中的識別字,因為像函式或變數名稱這類東西很可能會出現在程式碼的多個位置。在 MicroPython 中,駐留的字串稱為 QSTR(uniQue STRing,唯一字串)。
QSTR 值(型別為 qstr)是指向 QSTR 池鏈結串列的索引。QSTR 會儲存其長度以及內容的雜湊值,以便在去重複的過程中進行快速比較。所有處理字串的位元組碼運算都使用 QSTR 引數。
編譯期 QSTR 產生¶
在 MicroPython 的 C 程式碼中,任何應在最終韌體中被駐留的字串都會寫成 MP_QSTR_Foo。在編譯期,這會求值為一個 qstr 值,指向 QSTR 池中 "Foo" 的索引。
Makefile 中一個多步驟的流程讓這項機制得以運作。總結來說,此流程分為三個部分:
找出程式碼中所有的
MP_QSTR_Foo符記(token)。產生一個靜態的 QSTR 池,包含所有字串資料(含長度與雜湊值)。
(透過前置處理器)將所有
MP_QSTR_Foo替換為其對應的索引。
MP_QSTR_Foo 符記會在兩個來源中被搜尋:
$(SRC_QSTR)中參照的所有檔案。這是所有 C 程式碼(即py、extmod、ports/stm32),但不包含像lib這類第三方程式碼。額外的
$(QSTR_GLOBAL_DEPENDENCIES)(其中包含mpconfig*.h)。
注意: frozen_mpy.c(由 mpy-tool.py 產生)擁有自己的 QSTR 產生流程與池。
某些無法以 MP_QSTR_Foo 語法表達的額外字串(例如它們包含非英數字元),會透過 $(QSTR_DEFS) 變數在 qstrdefs.h 與 qstrdefsport.h 中明確提供。
處理過程分為以下幾個階段:
qstr.i.last是將每一個輸入檔案都送過 C 前置處理器後的串接結果。這代表任何依條件停用的程式碼都會被移除,且巨集會被展開。如此一來,我們就不會把不會用於最終韌體的字串加入池中。因為在這個階段(多虧了由QSTR_GEN_CFLAGS加入的NO_QSTR巨集),MP_QSTR_Foo沒有任何定義,所以它會原封不動地通過這個階段。這個檔案也包含前置處理器產生的、含有行號資訊的註解。請注意此步驟只使用已變更的檔案,這代表qstr.i.last只會包含自上次編譯以來已變更檔案的資料。qstr.split是在對 qstr.i.last 執行makeqstrdefs.py split之後所建立的空白檔案。它只是用來當作相依項目,以指示該步驟已執行。這個指令碼會為每個輸入的 C 檔案輸出一個檔案genhdr/qstr/...file.c.qstr,其中只包含匹配到的 QSTR。每個 QSTR 都會印成Q(Foo)。這個步驟是必要的,用以將既有的檔案與來自qstr.i.last增量更新所產生的新資料合併。qstrdefs.collected.h是使用makeqstrdefs.py cat串接genhdr/qstr/*的輸出結果。這就是程式碼中找到的完整MP_QSTR_Foo集合,現在格式化為每行一個Q(Foo),並含有重複項。只有在 qstr 集合有變動時,此檔案才會更新。QSTR 資料的雜湊值會寫入另一個檔案(qstrdefs.collected.h.hash),讓它能追蹤跨建置的變更。產生一個列舉(enumeration),其中每個項目將一個
MP_QSTR_Foo對應到其對應的索引。它會將qstrdefs.collected.h與qstrdefs*.h串接,然後將每一行從Q(Foo)轉換為"Q(Foo)",使其可原封不動地通過前置處理器。接著使用前置處理器來處理qstrdefs*.h中的任何條件編譯。然後再將轉換還原回Q(Foo),並儲存為qstrdefs.preprocessed.h。qstrdefs.generated.h是makeqstrdata.py的輸出。對於 qstrdefs.preprocessed.h 中的每個Q(Foo)(加上一些額外的硬編碼項目),它會輸出QDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo")。
接著在主編譯過程中,qstrdefs.generated.h 會發生兩件事:
在 qstr.h 中,每個 QDEF 都會成為列舉中的一個項目,這使得
MP_QSTR_Foo可供程式碼使用,並等於該字串在 QSTR 表格中的索引。在 qstr.c 中,實際的 QSTR 資料表格會以
mp_qstr_const_pool->qstrs的元素形式產生。
執行階段 QSTR 產生¶
可在執行階段建立額外的 QSTR 池,以便將字串加入其中。例如以下程式碼:
foo[x] = 3
將需要為 x 的值建立一個 QSTR,這樣它才能被「load attr」位元組碼使用。
此外,在編譯 Python 程式碼時,識別字與字面值需要建立 QSTR。注意:只有短於 10 個字元的字面值才會成為 QSTR。這是因為堆積(heap)上的一般字串至少都要佔用 16 個位元組(一個 GC 區塊),而 QSTR 則允許它們更有效率地封裝進池中。
QSTR 池(以及儲存字串資料的底層「區塊」)會在堆積上依需求以最小尺寸配置。