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 中一個多步驟的流程讓這項機制得以運作。總結來說,此流程分為三個部分:

  1. 找出程式碼中所有的 MP_QSTR_Foo 符記(token)。

  2. 產生一個靜態的 QSTR 池,包含所有字串資料(含長度與雜湊值)。

  3. (透過前置處理器)將所有 MP_QSTR_Foo 替換為其對應的索引。

MP_QSTR_Foo 符記會在兩個來源中被搜尋:

  1. $(SRC_QSTR) 中參照的所有檔案。這是所有 C 程式碼(即 pyextmodports/stm32),但不包含像 lib 這類第三方程式碼。

  2. 額外的 $(QSTR_GLOBAL_DEPENDENCIES)(其中包含 mpconfig*.h)。

注意: frozen_mpy.c(由 mpy-tool.py 產生)擁有自己的 QSTR 產生流程與池。

某些無法以 MP_QSTR_Foo 語法表達的額外字串(例如它們包含非英數字元),會透過 $(QSTR_DEFS) 變數在 qstrdefs.hqstrdefsport.h 中明確提供。

處理過程分為以下幾個階段:

  1. qstr.i.last 是將每一個輸入檔案都送過 C 前置處理器後的串接結果。這代表任何依條件停用的程式碼都會被移除,且巨集會被展開。如此一來,我們就不會把不會用於最終韌體的字串加入池中。因為在這個階段(多虧了由 QSTR_GEN_CFLAGS 加入的 NO_QSTR 巨集),MP_QSTR_Foo 沒有任何定義,所以它會原封不動地通過這個階段。這個檔案也包含前置處理器產生的、含有行號資訊的註解。請注意此步驟只使用已變更的檔案,這代表 qstr.i.last 只會包含自上次編譯以來已變更檔案的資料。

  2. qstr.split 是在對 qstr.i.last 執行 makeqstrdefs.py split 之後所建立的空白檔案。它只是用來當作相依項目,以指示該步驟已執行。這個指令碼會為每個輸入的 C 檔案輸出一個檔案 genhdr/qstr/...file.c.qstr,其中只包含匹配到的 QSTR。每個 QSTR 都會印成 Q(Foo)。這個步驟是必要的,用以將既有的檔案與來自 qstr.i.last 增量更新所產生的新資料合併。

  3. qstrdefs.collected.h 是使用 makeqstrdefs.py cat 串接 genhdr/qstr/* 的輸出結果。這就是程式碼中找到的完整 MP_QSTR_Foo 集合,現在格式化為每行一個 Q(Foo),並含有重複項。只有在 qstr 集合有變動時,此檔案才會更新。QSTR 資料的雜湊值會寫入另一個檔案(qstrdefs.collected.h.hash),讓它能追蹤跨建置的變更。

  4. 產生一個列舉(enumeration),其中每個項目將一個 MP_QSTR_Foo 對應到其對應的索引。它會將 qstrdefs.collected.hqstrdefs*.h 串接,然後將每一行從 Q(Foo) 轉換為 "Q(Foo)",使其可原封不動地通過前置處理器。接著使用前置處理器來處理 qstrdefs*.h 中的任何條件編譯。然後再將轉換還原回 Q(Foo),並儲存為 qstrdefs.preprocessed.h

  5. qstrdefs.generated.hmakeqstrdata.py 的輸出。對於 qstrdefs.preprocessed.h 中的每個 Q(Foo)(加上一些額外的硬編碼項目),它會輸出 QDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo")

接著在主編譯過程中,qstrdefs.generated.h 會發生兩件事:

  1. 在 qstr.h 中,每個 QDEF 都會成為列舉中的一個項目,這使得 MP_QSTR_Foo 可供程式碼使用,並等於該字串在 QSTR 表格中的索引。

  2. 在 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 池(以及儲存字串資料的底層「區塊」)會在堆積上依需求以最小尺寸配置。