Internace řetězců v MicroPythonu

MicroPython používá string interning k úspoře RAM i ROM. Tím se vyhne ukládání duplicitních kopií stejného řetězce. To se týká především identifikátorů ve vašem kódu, protože něco jako název funkce nebo proměnné se v kódu velmi pravděpodobně objeví na více místech. V MicroPythonu se internovaný řetězec nazývá QSTR (uniQue STRing, jedinečný řetězec).

Hodnota QSTR (typu qstr) je index do spojového seznamu zásobníků QSTR. QSTR ukládají svou délku a hash svého obsahu pro rychlé porovnání během procesu odstraňování duplicit. Všechny bytecodové operace, které pracují s řetězci, používají argument typu QSTR.

Generování QSTR při kompilaci

V C kódu MicroPythonu se jakékoli řetězce, které mají být internovány ve výsledném firmwaru, zapisují jako MP_QSTR_Foo. Při kompilaci se to vyhodnotí na hodnotu qstr, která ukazuje na index řetězce "Foo" v zásobníku QSTR.

Tuto funkcionalitu zajišťuje vícekrokový proces v souboru Makefile. Tento proces se ve zkratce skládá ze tří částí:

  1. Najít v kódu všechny tokeny MP_QSTR_Foo.

  2. Vygenerovat statický zásobník QSTR obsahující veškerá řetězcová data (včetně délek a hashů).

  3. Nahradit všechny MP_QSTR_Foo (prostřednictvím preprocesoru) jejich odpovídajícími indexy.

Tokeny MP_QSTR_Foo se vyhledávají ve dvou zdrojích:

  1. Ve všech souborech odkazovaných v $(SRC_QSTR). To je veškerý C kód (tj. py, extmod, ports/stm32), avšak nezahrnuje kód třetích stran, jako je lib.

  2. V dodatečných $(QSTR_GLOBAL_DEPENDENCIES) (které zahrnují mpconfig*.h).

Poznámka: frozen_mpy.c (generovaný nástrojem mpy-tool.py) má vlastní generování QSTR a vlastní zásobník.

Některé dodatečné řetězce, které nelze vyjádřit pomocí syntaxe MP_QSTR_Foo (např. obsahují nealfanumerické znaky), jsou explicitně uvedeny v souborech qstrdefs.h a qstrdefsport.h prostřednictvím proměnné $(QSTR_DEFS).

Zpracování probíhá v následujících fázích:

  1. qstr.i.last je výsledek spojení všech jednotlivých vstupních souborů, které prošly C preprocesorem. To znamená, že jakýkoli podmíněně vypnutý kód bude odstraněn a makra budou rozvinuta. Díky tomu nepřidáváme do zásobníku řetězce, které ve výsledném firmwaru nebudou použity. Protože v této fázi (díky makru NO_QSTR přidanému přes QSTR_GEN_CFLAGS) neexistuje žádná definice pro MP_QSTR_Foo, projde tato fáze beze změny. Tento soubor také obsahuje komentáře z preprocesoru, které zahrnují informace o číslech řádků. Všimněte si, že tento krok používá pouze soubory, které se změnily, což znamená, že qstr.i.last bude obsahovat pouze data ze souborů, které se od poslední kompilace změnily.

  2. qstr.split je prázdný soubor vytvořený po spuštění makeqstrdefs.py split na qstr.i.last. Slouží pouze jako závislost indikující, že daný krok proběhl. Tento skript vytváří jeden soubor pro každý vstupní C soubor, genhdr/qstr/...file.c.qstr, který obsahuje pouze odpovídající QSTR. Každý QSTR je vypsán jako Q(Foo). Tento krok je nezbytný pro spojení existujících souborů s novými daty vygenerovanými při inkrementální aktualizaci v qstr.i.last.

  3. qstrdefs.collected.h je výsledek spojení genhdr/qstr/* pomocí makeqstrdefs.py cat. Jedná se nyní o úplnou množinu MP_QSTR_Foo nalezených v kódu, formátovaných jako Q(Foo), jeden na řádek, s duplikáty. Tento soubor se aktualizuje pouze tehdy, pokud se změnila množina qstr. Hash dat QSTR se zapisuje do jiného souboru (qstrdefs.collected.h.hash), což umožňuje sledovat změny mezi sestaveními.

  4. Vygeneruje výčet (enum), v němž každá položka mapuje MP_QSTR_Foo na jeho odpovídající index. Spojí qstrdefs.collected.h s qstrdefs*.h a poté transformuje každý řádek z Q(Foo) na "Q(Foo)", aby prošly preprocesorem beze změny. Poté je preprocesor použit k vyřešení jakékoli podmíněné kompilace v qstrdefs*.h. Poté je transformace vrácena zpět na Q(Foo) a uložena jako qstrdefs.preprocessed.h.

  5. qstrdefs.generated.h je výstup makeqstrdata.py. Pro každé Q(Foo) v qstrdefs.preprocessed.h (plus některé další pevně zakódované) vypíše QDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").

Poté se při hlavní kompilaci se souborem qstrdefs.generated.h stanou dvě věci:

  1. V qstr.h se každý QDEF stane položkou výčtu (enum), což zpřístupní MP_QSTR_Foo kódu a učiní jej rovným indexu daného řetězce v tabulce QSTR.

  2. V qstr.c je vlastní datová tabulka QSTR generována jako prvky mp_qstr_const_pool->qstrs.

Generování QSTR za běhu

Za běhu lze vytvořit další zásobníky QSTR, aby do nich bylo možné přidávat řetězce. Například kód:

foo[x] = 3

Bude potřeba vytvořit QSTR pro hodnotu x, aby ji mohl použít bytecode „load attr“.

Také při kompilaci Python kódu je potřeba vytvořit QSTR pro identifikátory a literály. Poznámka: pouze literály kratší než 10 znaků se stávají QSTR. Důvodem je, že běžný řetězec na haldě vždy zabírá minimálně 16 bajtů (jeden GC blok), zatímco QSTR umožňují jejich efektivnější uspořádání do zásobníku.

Zásobníky QSTR (a podkladové „chunky“, které ukládají řetězcová data) jsou alokovány na haldě na vyžádání s minimální velikostí.