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
中定义的多步操作来完成的。总结来说,这个过程有三个部分:
在代码中找到所有的
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
语法表达的字符串,比如它们包含非字母数字字符,则需要在 qstrdefs.h
和 qstrdefsport.h
中通过 $(QSTR_DEFS)
变量进行指定。
处理过程会在以下阶段进行:
qstr.i.last
是将所有输入文件通过 C 预处理器进行合并的结果。因此,任何不满足条件的代码都会被删除,并且宏会被替换。这意味着我们未添加到池中的字符串,它们在最终的固件中无法被使用。因为在这一阶段(得益于QSTR_GEN_CFLAGS
添加的NO_QSTR
宏),MP_QSTR_Foo
尚未被定义,所以它会在这一阶段不受影响。这个文件还包含了预处理器中的注释,其中包含行号信息。注意,这一步只使用了文件已经改变的数据,这意味着qstr.i.last
将只包含从上次编译后改变的文件中的数据。qstr.split
是在运行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 池(和存储字符串数据的基本块 “chunks” )是在堆上使用最小尺寸按需分配的。