L’interning delle stringhe in MicroPython¶
MicroPython utilizza l”string interning per risparmiare sia RAM che ROM. Questo evita di dover memorizzare copie duplicate della stessa stringa. Principalmente, ciò si applica agli identificatori nel tuo codice, dato che qualcosa come il nome di una funzione o di una variabile è molto probabile che compaia in più punti del codice. In MicroPython una stringa interned è chiamata QSTR (uniQue STRing).
Un valore QSTR (con tipo qstr) è un indice in una lista concatenata di pool di QSTR. I QSTR memorizzano la propria lunghezza e un hash del proprio contenuto per un confronto rapido durante il processo di de-duplicazione. Tutte le operazioni del bytecode che lavorano con le stringhe usano un argomento QSTR.
Generazione dei QSTR al momento della compilazione¶
Nel codice C di MicroPython, qualsiasi stringa che debba essere interned nel firmware finale viene scritta come MP_QSTR_Foo. Al momento della compilazione questo verrà valutato come un valore qstr che punta all’indice di "Foo" nel pool di QSTR.
Un processo a più fasi nel Makefile fa funzionare tutto questo. In sintesi questo processo è composto da tre parti:
Trova tutti i token
MP_QSTR_Foonel codice.Genera un pool statico di QSTR contenente tutti i dati delle stringhe (incluse lunghezze e hash).
Sostituisce tutti i
MP_QSTR_Foo(tramite il preprocessore) con il loro indice corrispondente.
I token MP_QSTR_Foo vengono cercati in due sorgenti:
Tutti i file referenziati in
$(SRC_QSTR). Questo è tutto il codice C (cioèpy,extmod,ports/stm32) ma non include il codice di terze parti comelib.Ulteriori
$(QSTR_GLOBAL_DEPENDENCIES)(che includonompconfig*.h).
Nota: frozen_mpy.c (generato da mpy-tool.py) ha la propria generazione e il proprio pool di QSTR.
Alcune stringhe aggiuntive che non possono essere espresse usando la sintassi MP_QSTR_Foo (ad esempio perché contengono caratteri non alfanumerici) vengono fornite esplicitamente in qstrdefs.h e qstrdefsport.h tramite la variabile $(QSTR_DEFS).
L’elaborazione avviene nelle seguenti fasi:
qstr.i.lastè la concatenazione del risultato del passaggio di ogni singolo file di input attraverso il preprocessore C. Questo significa che qualsiasi codice disabilitato condizionalmente verrà rimosso, e le macro espanse. Questo significa che non aggiungiamo al pool stringhe che non saranno usate nel firmware finale. Poiché in questa fase (grazie alla macroNO_QSTRaggiunta daQSTR_GEN_CFLAGS) non c’è alcuna definizione perMP_QSTR_Foo, esso attraversa questa fase senza essere modificato. Questo file include anche commenti dal preprocessore che contengono informazioni sui numeri di riga. Nota che questo passaggio usa solo i file che sono cambiati, il che significa cheqstr.i.lastconterrà solo dati provenienti dai file che sono cambiati dall’ultima compilazione.qstr.splitè un file vuoto creato dopo l’esecuzione dimakeqstrdefs.py splitsu qstr.i.last. Viene usato semplicemente come dipendenza per indicare che il passaggio è stato eseguito. Questo script produce un file per ciascun file C di input,genhdr/qstr/...file.c.qstr, che contiene solo i QSTR trovati. Ogni QSTR viene stampato comeQ(Foo). Questo passaggio è necessario per combinare i file esistenti con i nuovi dati generati dall’aggiornamento incrementale inqstr.i.last.qstrdefs.collected.hè il risultato della concatenazione digenhdr/qstr/*usandomakeqstrdefs.py cat. Questo è ora l’insieme completo deiMP_QSTR_Footrovati nel codice, ora formattati comeQ(Foo), uno per riga, con i duplicati. Questo file viene aggiornato solo se l’insieme dei qstr è cambiato. Un hash dei dati QSTR viene scritto in un altro file (qstrdefs.collected.h.hash) che consente di tracciare i cambiamenti tra le build.Genera un’enumerazione, ciascuna voce della quale mappa un
MP_QSTR_Fooal suo indice corrispondente. Concatenaqstrdefs.collected.hconqstrdefs*.h, poi trasforma ogni riga daQ(Foo)a"Q(Foo)"in modo che attraversino il preprocessore senza modifiche. Poi il preprocessore viene usato per gestire qualsiasi compilazione condizionale inqstrdefs*.h. Poi la trasformazione viene annullata tornando aQ(Foo), e salvata comeqstrdefs.preprocessed.h.qstrdefs.generated.hè il risultato dimakeqstrdata.py. Per ogniQ(Foo)in qstrdefs.preprocessed.h (più alcuni hard-coded aggiuntivi), produceQDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").
Poi nella compilazione principale, accadono due cose con qstrdefs.generated.h:
In qstr.h, ogni QDEF diventa una voce in un enum, il che rende
MP_QSTR_Foodisponibile al codice e pari all’indice di quella stringa nella tabella QSTR.In qstr.c, l’effettiva tabella dei dati QSTR viene generata come elementi di
mp_qstr_const_pool->qstrs.
Generazione dei QSTR a runtime¶
Pool di QSTR aggiuntivi possono essere creati a runtime in modo che vi si possano aggiungere stringhe. Per esempio, il codice:
foo[x] = 3
Dovrà creare un QSTR per il valore di x in modo che possa essere usato dal bytecode «load attr».
Inoltre, durante la compilazione del codice Python, è necessario creare QSTR per identificatori e letterali. Nota: solo i letterali più corti di 10 caratteri diventano QSTR. Questo perché una normale stringa nello heap occupa sempre un minimo di 16 byte (un blocco GC), mentre i QSTR consentono di impacchettarli in modo più efficiente nel pool.
I pool di QSTR (e i «chunk» sottostanti che memorizzano i dati delle stringhe) vengono allocati su richiesta nello heap con una dimensione minima.