Interning string MicroPython

MicroPython menggunakan string interning untuk menghemat RAM maupun ROM. Ini menghindari penyimpanan salinan duplikat dari string yang sama. Terutama, ini berlaku untuk identifier dalam kode Anda, karena sesuatu seperti nama fungsi atau variabel sangat mungkin muncul di beberapa tempat dalam kode. Dalam MicroPython, string yang di-intern disebut QSTR (uniQue STRing).

Nilai QSTR (dengan tipe qstr) adalah indeks ke dalam linked list dari pool QSTR. QSTR menyimpan panjang dan hash dari kontennya untuk perbandingan cepat selama proses de-duplikasi. Semua operasi bytecode yang bekerja dengan string menggunakan argumen QSTR.

Pembuatan QSTR saat kompilasi

Dalam kode C MicroPython, string apa pun yang harus di-intern dalam firmware akhir ditulis sebagai MP_QSTR_Foo. Pada saat kompilasi, ini akan dievaluasi menjadi nilai qstr yang menunjuk ke indeks "Foo" dalam pool QSTR.

Proses multi-langkah dalam Makefile membuat ini bekerja. Secara ringkas, proses ini memiliki tiga bagian:

  1. Temukan semua token MP_QSTR_Foo dalam kode.

  2. Buat pool QSTR statis yang berisi semua data string (termasuk panjang dan hash).

  3. Ganti semua MP_QSTR_Foo (melalui preprocessor) dengan indeks yang sesuai.

Token MP_QSTR_Foo dicari di dua sumber:

  1. Semua file yang direferensikan dalam $(SRC_QSTR). Ini adalah semua kode C (yaitu py, extmod, ports/stm32) tetapi tidak termasuk kode pihak ketiga seperti lib.

  2. $(QSTR_GLOBAL_DEPENDENCIES) tambahan (yang mencakup mpconfig*.h).

Catatan: frozen_mpy.c (dibuat oleh mpy-tool.py) memiliki pembuatan QSTR dan pool-nya sendiri.

Beberapa string tambahan yang tidak dapat diekspresikan menggunakan sintaks MP_QSTR_Foo (misalnya mengandung karakter non-alfanumerik) disediakan secara eksplisit dalam qstrdefs.h dan qstrdefsport.h melalui variabel $(QSTR_DEFS).

Pemrosesan terjadi dalam tahapan berikut:

  1. qstr.i.last adalah penggabungan dari memasukkan setiap file input tunggal melalui preprocessor C. Ini berarti kode yang dinonaktifkan secara kondisional akan dihapus, dan makro diperluas. Ini berarti kita tidak menambahkan string ke pool yang tidak akan digunakan dalam firmware akhir. Karena pada tahap ini (berkat makro NO_QSTR yang ditambahkan oleh QSTR_GEN_CFLAGS) tidak ada definisi untuk MP_QSTR_Foo, maka ia melewati tahap ini tanpa terpengaruh. File ini juga mencakup komentar dari preprocessor yang berisi informasi nomor baris. Perhatikan bahwa langkah ini hanya menggunakan file yang telah berubah, yang berarti qstr.i.last hanya akan berisi data dari file yang telah berubah sejak kompilasi terakhir.

  2. qstr.split adalah file kosong yang dibuat setelah menjalankan makeqstrdefs.py split pada qstr.i.last. File ini hanya digunakan sebagai dependensi untuk menunjukkan bahwa langkah tersebut telah berjalan. Skrip ini menghasilkan satu file per file C input, genhdr/qstr/...file.c.qstr, yang hanya berisi QSTR yang cocok. Setiap QSTR dicetak sebagai Q(Foo). Langkah ini diperlukan untuk menggabungkan file yang ada dengan data baru yang dihasilkan dari pembaruan inkremental dalam qstr.i.last.

  3. qstrdefs.collected.h adalah output dari penggabungan genhdr/qstr/* menggunakan makeqstrdefs.py cat. Ini sekarang merupakan kumpulan lengkap MP_QSTR_Foo yang ditemukan dalam kode, sekarang diformat sebagai Q(Foo), satu per baris, dengan duplikat. File ini hanya diperbarui jika kumpulan qstr telah berubah. Hash dari data QSTR ditulis ke file lain (qstrdefs.collected.h.hash) yang memungkinkannya melacak perubahan antar build.

  4. Buat enumerasi, yang setiap entrinya memetakan MP_QSTR_Foo ke indeks yang sesuai. Enumerasi ini menggabungkan qstrdefs.collected.h dengan qstrdefs*.h, kemudian mengubah setiap baris dari Q(Foo) menjadi "Q(Foo)" agar melewati preprocessor tanpa perubahan. Kemudian preprocessor digunakan untuk menangani kompilasi kondisional apa pun dalam qstrdefs*.h. Kemudian transformasi dibatalkan kembali ke Q(Foo), dan disimpan sebagai qstrdefs.preprocessed.h.

  5. qstrdefs.generated.h adalah output dari makeqstrdata.py. Untuk setiap Q(Foo) dalam qstrdefs.preprocessed.h (ditambah beberapa yang di-hardcode), ia menghasilkan QDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").

Kemudian dalam kompilasi utama, dua hal terjadi dengan qstrdefs.generated.h:

  1. Dalam qstr.h, setiap QDEF menjadi entri dalam enum, yang membuat MP_QSTR_Foo tersedia untuk kode dan sama dengan indeks string tersebut dalam tabel QSTR.

  2. Dalam qstr.c, tabel data QSTR aktual dibuat sebagai elemen dari mp_qstr_const_pool->qstrs.

Pembuatan QSTR saat runtime

Pool QSTR tambahan dapat dibuat saat runtime sehingga string dapat ditambahkan ke dalamnya. Sebagai contoh, kode berikut:

foo[x] = 3

Perlu membuat QSTR untuk nilai x agar dapat digunakan oleh bytecode "load attr".

Selain itu, saat mengompilasi kode Python, identifier dan literal perlu dibuat QSTRnya. Catatan: hanya literal yang lebih pendek dari 10 karakter yang menjadi QSTR. Ini karena string biasa di heap selalu menggunakan setidaknya 16 byte (satu blok GC), sedangkan QSTR memungkinkan mereka dikemas lebih efisien ke dalam pool.

Pool QSTR (dan "chunk" yang mendasarinya yang menyimpan data string) dialokasikan sesuai permintaan di heap dengan ukuran minimum.