Internowanie łańcuchów znaków w MicroPython¶
MicroPython używa string interning, aby oszczędzać zarówno pamięć RAM, jak i ROM. Pozwala to uniknąć przechowywania duplikatów tego samego łańcucha znaków. Dotyczy to przede wszystkim identyfikatorów w Twoim kodzie, ponieważ coś takiego jak nazwa funkcji lub zmiennej z dużym prawdopodobieństwem pojawia się w wielu miejscach w kodzie. W MicroPython internowany łańcuch znaków nazywany jest QSTR (uniQue STRing).
Wartość QSTR (typu qstr) jest indeksem w liście powiązanej pul QSTR. QSTR przechowują swoją długość oraz hash swojej zawartości w celu szybkiego porównywania podczas procesu usuwania duplikatów. Wszystkie operacje na kodzie bajtowym, które działają na łańcuchach znaków, używają argumentu QSTR.
Generowanie QSTR w czasie kompilacji¶
W kodzie C MicroPython wszystkie łańcuchy znaków, które powinny być internowane w końcowym oprogramowaniu układowym, są zapisywane jako MP_QSTR_Foo. W czasie kompilacji zostanie to obliczone do wartości qstr, która wskazuje na indeks "Foo" w puli QSTR.
Wieloetapowy proces w pliku Makefile sprawia, że to działa. W skrócie proces ten składa się z trzech części:
Znalezienie wszystkich tokenów
MP_QSTR_Foow kodzie.Wygenerowanie statycznej puli QSTR zawierającej wszystkie dane łańcuchów znaków (w tym długości i hashe).
Zastąpienie wszystkich
MP_QSTR_Foo(za pomocą preprocesora) ich odpowiednimi indeksami.
Tokeny MP_QSTR_Foo są wyszukiwane w dwóch źródłach:
Wszystkie pliki przywoływane w
$(SRC_QSTR). Jest to cały kod C (tzn.py,extmod,ports/stm32), ale bez kodu firm trzecich, takiego jaklib.Dodatkowe
$(QSTR_GLOBAL_DEPENDENCIES)(które obejmująmpconfig*.h).
Uwaga: frozen_mpy.c (generowany przez mpy-tool.py) ma własne generowanie QSTR oraz własną pulę.
Niektóre dodatkowe łańcuchy znaków, których nie da się wyrazić za pomocą składni MP_QSTR_Foo (np. zawierające znaki niealfanumeryczne), są podawane jawnie w plikach qstrdefs.h i qstrdefsport.h za pośrednictwem zmiennej $(QSTR_DEFS).
Przetwarzanie odbywa się w następujących etapach:
qstr.i.lastjest wynikiem konkatenacji wszystkich pojedynczych plików wejściowych przepuszczonych przez preprocesor C. Oznacza to, że cały kod warunkowo wyłączony zostanie usunięty, a makra rozwinięte. Dzięki temu nie dodajemy do puli łańcuchów znaków, które nie będą używane w końcowym oprogramowaniu układowym. Ponieważ na tym etapie (dzięki makruNO_QSTRdodanemu przezQSTR_GEN_CFLAGS) nie ma definicji dlaMP_QSTR_Foo, przechodzi on przez ten etap bez zmian. Plik ten zawiera również komentarze pochodzące z preprocesora, które zawierają informacje o numerach wierszy. Należy zauważyć, że ten krok wykorzystuje tylko pliki, które uległy zmianie, co oznacza, żeqstr.i.lastbędzie zawierać dane wyłącznie z plików zmienionych od ostatniej kompilacji.qstr.splitjest pustym plikiem tworzonym po uruchomieniumakeqstrdefs.py splitna qstr.i.last. Służy on po prostu jako zależność wskazująca, że dany krok został wykonany. Skrypt ten generuje jeden plik na każdy wejściowy plik C,genhdr/qstr/...file.c.qstr, który zawiera wyłącznie dopasowane QSTR. Każdy QSTR jest wypisywany jakoQ(Foo). Ten krok jest niezbędny, aby połączyć istniejące pliki z nowymi danymi wygenerowanymi z przyrostowej aktualizacji wqstr.i.last.qstrdefs.collected.hjest wynikiem konkatenacjigenhdr/qstr/*za pomocąmakeqstrdefs.py cat. Jest to teraz pełny zbiórMP_QSTR_Fooznalezionych w kodzie, sformatowanych obecnie jakoQ(Foo), po jednym w wierszu, z duplikatami. Plik ten jest aktualizowany tylko wtedy, gdy zbiór qstr uległ zmianie. Hash danych QSTR jest zapisywany w innym pliku (qstrdefs.collected.h.hash), co pozwala śledzić zmiany pomiędzy kolejnymi kompilacjami.Wygenerowanie enumeracji, której każdy wpis odwzorowuje
MP_QSTR_Foona odpowiadający mu indeks. Konkatenuje onaqstrdefs.collected.hzqstrdefs*.h, a następnie przekształca każdy wiersz zQ(Foo)na"Q(Foo)", tak aby przeszły przez preprocesor bez zmian. Następnie preprocesor jest używany do obsługi wszelkiej kompilacji warunkowej wqstrdefs*.h. Potem przekształcenie jest cofane z powrotem doQ(Foo)i zapisywane jakoqstrdefs.preprocessed.h.qstrdefs.generated.hjest wynikiem działaniamakeqstrdata.py. Dla każdegoQ(Foo)w qstrdefs.preprocessed.h (oraz kilku dodatkowych, zakodowanych na stałe) generuje onQDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").
Następnie w głównej kompilacji z qstrdefs.generated.h dzieją się dwie rzeczy:
W qstr.h każdy QDEF staje się wpisem w enumeracji, co sprawia, że
MP_QSTR_Foojest dostępny dla kodu i równy indeksowi tego łańcucha znaków w tablicy QSTR.W qstr.c generowana jest właściwa tablica danych QSTR jako elementy
mp_qstr_const_pool->qstrs.
Generowanie QSTR w czasie wykonywania¶
Dodatkowe pule QSTR mogą być tworzone w czasie wykonywania, tak aby można było dodawać do nich łańcuchy znaków. Na przykład kod:
foo[x] = 3
Będzie musiał utworzyć QSTR dla wartości x, aby mógł on zostać użyty przez kod bajtowy „load attr”.
Ponadto, podczas kompilacji kodu Python, dla identyfikatorów i literałów muszą zostać utworzone QSTR. Uwaga: tylko literały krótsze niż 10 znaków stają się QSTR. Dzieje się tak, ponieważ zwykły łańcuch znaków na stercie zawsze zajmuje co najmniej 16 bajtów (jeden blok GC), podczas gdy QSTR pozwalają na ich bardziej efektywne upakowanie w puli.
Pule QSTR (oraz leżące u ich podstaw „chunki” przechowujące dane łańcuchów znaków) są alokowane na żądanie na stercie z minimalnym rozmiarem.