Internado de cadenas en MicroPython¶
MicroPython usa el string interning (internado de cadenas) para ahorrar tanto RAM como ROM. Esto evita tener que almacenar copias duplicadas de la misma cadena. Principalmente, esto se aplica a los identificadores de tu código, ya que algo como el nombre de una función o de una variable es muy probable que aparezca en varios lugares del código. En MicroPython una cadena internada se denomina QSTR (uniQue STRing).
Un valor QSTR (de tipo qstr) es un índice dentro de una lista enlazada de grupos (pools) de QSTR. Los QSTR almacenan su longitud y un hash de su contenido para una comparación rápida durante el proceso de deduplicación. Todas las operaciones de bytecode que trabajan con cadenas usan un argumento QSTR.
Generación de QSTR en tiempo de compilación¶
En el código C de MicroPython, cualquier cadena que deba internarse en el firmware final se escribe como MP_QSTR_Foo. En tiempo de compilación esto se evalúa como un valor qstr que apunta al índice de "Foo" en el grupo de QSTR.
Un proceso de varios pasos en el Makefile hace que esto funcione. En resumen, este proceso tiene tres partes:
Encontrar todos los tokens
MP_QSTR_Fooen el código.Generar un grupo estático de QSTR que contenga todos los datos de las cadenas (incluyendo longitudes y hashes).
Reemplazar todos los
MP_QSTR_Foo(mediante el preprocesador) por su índice correspondiente.
Los tokens MP_QSTR_Foo se buscan en dos fuentes:
Todos los archivos referenciados en
$(SRC_QSTR). Esto incluye todo el código C (es decir,py,extmod,ports/stm32) pero no el código de terceros comolib.Los
$(QSTR_GLOBAL_DEPENDENCIES)adicionales (que incluyenmpconfig*.h).
Nota: frozen_mpy.c (generado por mpy-tool.py) tiene su propia generación y grupo de QSTR.
Algunas cadenas adicionales que no pueden expresarse usando la sintaxis MP_QSTR_Foo (por ejemplo, porque contienen caracteres no alfanuméricos) se proporcionan explícitamente en qstrdefs.h y qstrdefsport.h mediante la variable $(QSTR_DEFS).
El procesamiento ocurre en las siguientes etapas:
qstr.i.lastes la concatenación resultante de pasar cada uno de los archivos de entrada por el preprocesador de C. Esto significa que cualquier código desactivado condicionalmente se eliminará y las macros se expandirán. Esto significa que no añadimos al grupo cadenas que no se usarán en el firmware final. Porque en esta etapa (gracias a la macroNO_QSTRañadida porQSTR_GEN_CFLAGS) no hay definición paraMP_QSTR_Foo, este pasa por esta etapa sin verse afectado. Este archivo también incluye comentarios del preprocesador que contienen información sobre los números de línea. Ten en cuenta que este paso solo usa archivos que han cambiado, lo que significa queqstr.i.lastsolo contendrá datos de los archivos que hayan cambiado desde la última compilación.qstr.splites un archivo vacío creado tras ejecutarmakeqstrdefs.py splitsobre qstr.i.last. Solo se usa como dependencia para indicar que el paso se ejecutó. Este script genera un archivo por cada archivo C de entrada,genhdr/qstr/...file.c.qstr, que contiene únicamente los QSTR coincidentes. Cada QSTR se imprime comoQ(Foo). Este paso es necesario para combinar los archivos existentes con los nuevos datos generados a partir de la actualización incremental enqstr.i.last.qstrdefs.collected.hes el resultado de concatenargenhdr/qstr/*usandomakeqstrdefs.py cat. Este es ahora el conjunto completo deMP_QSTR_Fooencontrados en el código, ahora formateados comoQ(Foo), uno por línea, con duplicados. Este archivo solo se actualiza si el conjunto de qstrs ha cambiado. Se escribe un hash de los datos de QSTR en otro archivo (qstrdefs.collected.h.hash) que permite rastrear los cambios entre compilaciones.Genera una enumeración, cada entrada de la cual asigna un
MP_QSTR_Fooa su índice correspondiente. Concatenaqstrdefs.collected.hconqstrdefs*.hy luego transforma cada línea deQ(Foo)a"Q(Foo)"para que pasen por el preprocesador sin cambios. Después se usa el preprocesador para tratar cualquier compilación condicional enqstrdefs*.h. Luego se deshace la transformación de vuelta aQ(Foo)y se guarda comoqstrdefs.preprocessed.h.qstrdefs.generated.hes el resultado demakeqstrdata.py. Por cadaQ(Foo)en qstrdefs.preprocessed.h (más algunos otros codificados directamente), generaQDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").
Después, en la compilación principal, ocurren dos cosas con qstrdefs.generated.h:
En qstr.h, cada QDEF se convierte en una entrada de un enum, lo que hace que
MP_QSTR_Fooesté disponible para el código y sea igual al índice de esa cadena en la tabla de QSTR.En qstr.c, la tabla real de datos de QSTR se genera como elementos de
mp_qstr_const_pool->qstrs.
Generación de QSTR en tiempo de ejecución¶
Se pueden crear grupos adicionales de QSTR en tiempo de ejecución para poder añadirles cadenas. Por ejemplo, el código:
foo[x] = 3
Necesitará crear un QSTR para el valor de x para que pueda usarlo el bytecode «load attr».
Además, al compilar código Python, los identificadores y literales necesitan tener QSTR creados. Nota: solo los literales de menos de 10 caracteres se convierten en QSTR. Esto se debe a que una cadena normal en el montón (heap) siempre ocupa un mínimo de 16 bytes (un bloque de GC), mientras que los QSTR permiten empaquetarlos de forma más eficiente en el grupo.
Los grupos de QSTR (y los «chunks» subyacentes que almacenan los datos de las cadenas) se asignan bajo demanda en el montón (heap) con un tamaño mínimo.