L’internement de chaînes de MicroPython¶
MicroPython utilise l”string interning pour économiser à la fois de la RAM et de la ROM. Cela évite d’avoir à stocker des copies en double de la même chaîne. Cela s’applique principalement aux identifiants de votre code, car un élément tel qu’un nom de fonction ou de variable est très susceptible d’apparaître à plusieurs endroits du code. Dans MicroPython, une chaîne internée est appelée un QSTR (uniQue STRing).
Une valeur QSTR (de type qstr) est un index dans une liste chaînée de pools de QSTR. Les QSTR stockent leur longueur et un hash de leur contenu pour une comparaison rapide lors du processus de déduplication. Toutes les opérations de bytecode qui manipulent des chaînes utilisent un argument QSTR.
Génération des QSTR à la compilation¶
Dans le code C de MicroPython, toutes les chaînes qui doivent être internées dans le micrologiciel final sont écrites sous la forme MP_QSTR_Foo. À la compilation, cela donnera une valeur qstr qui pointe vers l’index de "Foo" dans le pool de QSTR.
Un processus en plusieurs étapes dans le Makefile permet d’obtenir ce résultat. En résumé, ce processus comporte trois parties :
Trouver tous les jetons
MP_QSTR_Foodans le code.Générer un pool de QSTR statique contenant toutes les données des chaînes (y compris les longueurs et les hash).
Remplacer tous les
MP_QSTR_Foo(via le préprocesseur) par leur index correspondant.
Les jetons MP_QSTR_Foo sont recherchés dans deux sources :
Tous les fichiers référencés dans
$(SRC_QSTR). Il s’agit de tout le code C (c.-à-d.py,extmod,ports/stm32) mais sans inclure le code tiers tel quelib.Les
$(QSTR_GLOBAL_DEPENDENCIES)supplémentaires (qui incluentmpconfig*.h).
Remarque : frozen_mpy.c (généré par mpy-tool.py) possède sa propre génération et son propre pool de QSTR.
Certaines chaînes supplémentaires qui ne peuvent pas être exprimées avec la syntaxe MP_QSTR_Foo (par exemple parce qu’elles contiennent des caractères non alphanumériques) sont fournies explicitement dans qstrdefs.h et qstrdefsport.h via la variable $(QSTR_DEFS).
Le traitement se déroule dans les étapes suivantes :
qstr.i.lastest le résultat de la concaténation obtenue en passant chacun des fichiers d’entrée dans le préprocesseur C. Cela signifie que tout code désactivé conditionnellement sera supprimé et que les macros seront expansées. Cela signifie que nous n’ajoutons pas au pool des chaînes qui ne seront pas utilisées dans le micrologiciel final. Parce qu’à ce stade (grâce à la macroNO_QSTRajoutée parQSTR_GEN_CFLAGS) il n’existe aucune définition pourMP_QSTR_Foo, celui-ci traverse cette étape sans être modifié. Ce fichier inclut également des commentaires du préprocesseur qui contiennent des informations sur les numéros de ligne. Notez que cette étape n’utilise que les fichiers qui ont changé, ce qui signifie queqstr.i.lastne contiendra que les données des fichiers ayant changé depuis la dernière compilation.qstr.splitest un fichier vide créé après l’exécution demakeqstrdefs.py splitsur qstr.i.last. Il sert simplement de dépendance pour indiquer que l’étape s’est exécutée. Ce script produit un fichier par fichier C d’entrée,genhdr/qstr/...file.c.qstr, qui ne contient que les QSTR correspondants. Chaque QSTR est imprimé sous la formeQ(Foo). Cette étape est nécessaire pour combiner les fichiers existants avec les nouvelles données générées par la mise à jour incrémentale dansqstr.i.last.qstrdefs.collected.hest le résultat de la concaténation degenhdr/qstr/*à l’aide demakeqstrdefs.py cat. Il s’agit désormais de l’ensemble complet desMP_QSTR_Footrouvés dans le code, maintenant formatés sous la formeQ(Foo), un par ligne, avec les doublons. Ce fichier n’est mis à jour que si l’ensemble des qstr a changé. Un hash des données QSTR est écrit dans un autre fichier (qstrdefs.collected.h.hash) qui permet de suivre les changements d’une compilation à l’autre.Génère une énumération, dont chaque entrée associe un
MP_QSTR_Fooà son index correspondant. Elle concatèneqstrdefs.collected.havecqstrdefs*.h, puis transforme chaque ligne deQ(Foo)en"Q(Foo)"afin qu’elles traversent le préprocesseur sans modification. Le préprocesseur est ensuite utilisé pour gérer toute compilation conditionnelle dansqstrdefs*.h. La transformation est ensuite annulée pour revenir àQ(Foo), et le résultat est enregistré sousqstrdefs.preprocessed.h.qstrdefs.generated.hest le résultat demakeqstrdata.py. Pour chaqueQ(Foo)dans qstrdefs.preprocessed.h (plus quelques entrées supplémentaires codées en dur), il produitQDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").
Ensuite, lors de la compilation principale, deux choses se produisent avec qstrdefs.generated.h :
Dans qstr.h, chaque QDEF devient une entrée d’un enum, ce qui rend
MP_QSTR_Foodisponible pour le code et égal à l’index de cette chaîne dans la table QSTR.Dans qstr.c, la table de données QSTR réelle est générée sous forme d’éléments de
mp_qstr_const_pool->qstrs.
Génération des QSTR à l’exécution¶
Des pools de QSTR supplémentaires peuvent être créés à l’exécution afin que des chaînes puissent y être ajoutées. Par exemple, le code suivant
foo[x] = 3
Devra créer un QSTR pour la valeur de x afin qu’elle puisse être utilisée par le bytecode « load attr ».
De plus, lors de la compilation du code Python, des QSTR doivent être créés pour les identifiants et les littéraux. Remarque : seuls les littéraux de moins de 10 caractères deviennent des QSTR. Cela s’explique par le fait qu’une chaîne ordinaire sur le tas occupe toujours au minimum 16 octets (un bloc GC), tandis que les QSTR permettent de les empaqueter plus efficacement dans le pool.
Les pools de QSTR (et les « chunks » sous-jacents qui stockent les données des chaînes) sont alloués à la demande sur le tas avec une taille minimale.