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 中的一个多步骤过程使其得以实现。简而言之,该过程分为三个部分:

  1. 在代码中查找所有 MP_QSTR_Foo 标记。

  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. 生成一个枚举,其中每个条目都将一个 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。这是因为堆上的常规字符串总是至少占用 16 字节(一个 GC 块),而 QSTR 则可以更高效地打包进池中。

QSTR 池(以及存储字符串数据的底层 “块”)会在堆上按需分配,并有一个最小大小。