MicroPython 字符串驻留

MicroPython 使用 string interning (字符串驻留)来节省 RAM 和 ROM 。这样可以避免了对于相同的字符串,存储了重复的引用。主要是因为在你的代码中,某些标识符,比如函数或变量名,很可能会出现在多个地方。在 MicroPython 中,一个驻留的字符串被称为 QSTR (uniQue STRing 的缩写,指唯一字符串)。

QSTR 值(以 qstr 类型存在)是一个链表中的 QSTR 值的索引。QSTR 存储了其长度和内容的哈希值,以便快速的比较。所有与字符串操作相关的 bytecode 操作都使用 QSTR 参数。

编译时进行的 QSTR 生成

在 MicroPython C 代码中,任何需要在最终的固件中驻留的字符串,都会写入为 MP_QSTR_Foo 。在编译时,这将会将其转换为一个 qstr 值,指向 "Foo" 在 QSTR 池中的索引。

这项工作,是通过在 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 语法表达的字符串,比如它们包含非字母数字字符,则需要在 qstrdefs.hqstrdefsport.h 中通过 $(QSTR_DEFS) 变量进行指定。

处理过程会在以下阶段进行:

  1. qstr.i.last 是将所有输入文件通过 C 预处理器进行合并的结果。因此,任何不满足条件的代码都会被删除,并且宏会被替换。这意味着我们未添加到池中的字符串,它们在最终的固件中无法被使用。因为在这一阶段(得益于 QSTR_GEN_CFLAGS 添加的 NO_QSTR 宏), MP_QSTR_Foo 尚未被定义,所以它会在这一阶段不受影响。这个文件还包含了预处理器中的注释,其中包含行号信息。注意,这一步只使用了文件已经改变的数据,这意味着 qstr.i.last 将只包含从上次编译后改变的文件中的数据。

  2. qstr.split 是在运行 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 池(和存储字符串数据的基本块 “chunks” )是在堆上使用最小尺寸按需分配的。