MicroPython 字符串驻留¶
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标记。生成一个包含所有字符串数据(包括长度和哈希值)的静态 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),以便跟踪跨构建的更改。生成一个枚举,其中每个条目都将一个
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。这是因为堆上的常规字符串总是至少占用 16 字节(一个 GC 块),而 QSTR 则可以更高效地打包进池中。
QSTR 池(以及存储字符串数据的底层 “块”)会在堆上按需分配,并有一个最小大小。