MicroPython dize içselleştirme (string interning)

MicroPython, hem RAM hem de ROM’dan tasarruf etmek için string interning kullanır. Bu, aynı dizenin yinelenen kopyalarını depolama zorunluluğunu ortadan kaldırır. Bu öncelikle kodunuzdaki tanımlayıcılar için geçerlidir; çünkü bir fonksiyon ya da değişken adı gibi bir şeyin kodun birden çok yerinde görünmesi çok olasıdır. MicroPython’da içselleştirilmiş bir dizeye QSTR (uniQue STRing) adı verilir.

Bir QSTR değeri (qstr tipinde) QSTR havuzlarından oluşan bağlı bir listeye yapılan bir indekstir. QSTR’ler, yinelemeleri kaldırma (de-duplication) işlemi sırasında hızlı karşılaştırma yapabilmek için uzunluklarını ve içeriklerinin bir hash’ini depolar. Dizelerle çalışan tüm bytecode işlemleri bir QSTR argümanı kullanır.

Derleme zamanı QSTR üretimi

MicroPython C kodunda, nihai aygıt yazılımında (firmware) içselleştirilmesi gereken her dize MP_QSTR_Foo olarak yazılır. Derleme zamanında bu, QSTR havuzundaki "Foo" indeksine işaret eden bir qstr değerine dönüşür.

Makefile içindeki çok adımlı bir süreç bunu mümkün kılar. Özetle bu süreç üç bölümden oluşur:

  1. Koddaki tüm MP_QSTR_Foo belirteçlerini (token) bul.

  2. Tüm dize verilerini (uzunluklar ve hash’ler dahil) içeren statik bir QSTR havuzu oluştur.

  3. Tüm MP_QSTR_Foo belirteçlerini (ön işlemci aracılığıyla) karşılık gelen indeksleriyle değiştir.

MP_QSTR_Foo belirteçleri iki kaynakta aranır:

  1. $(SRC_QSTR) içinde başvurulan tüm dosyalar. Bu, tüm C kodudur (yani py, extmod, ports/stm32) ancak lib gibi üçüncü taraf kodu hariçtir.

  2. Ek $(QSTR_GLOBAL_DEPENDENCIES) (mpconfig*.h dosyasını içerir).

Not: frozen_mpy.c (mpy-tool.py tarafından üretilir) kendi QSTR üretimine ve havuzuna sahiptir.

MP_QSTR_Foo söz dizimiyle ifade edilemeyen bazı ek dizeler (örneğin alfasayısal olmayan karakterler içerenler) açıkça qstrdefs.h ve qstrdefsport.h içinde $(QSTR_DEFS) değişkeni aracılığıyla sağlanır.

İşlem aşağıdaki aşamalarda gerçekleşir:

  1. qstr.i.last, her bir giriş dosyasının C ön işlemcisinden geçirilmesinin birleştirilmiş halidir. Bu, koşullu olarak devre dışı bırakılmış her kodun kaldırılacağı ve makroların genişletileceği anlamına gelir. Yani nihai aygıt yazılımında (firmware) kullanılmayacak dizeleri havuza eklemeyiz. Çünkü bu aşamada (QSTR_GEN_CFLAGS tarafından eklenen NO_QSTR makrosu sayesinde) MP_QSTR_Foo için bir tanım yoktur ve bu aşamadan değişmeden geçer. Bu dosya ayrıca ön işlemciden gelen, satır numarası bilgisi içeren yorumları da içerir. Bu adımın yalnızca değişmiş dosyaları kullandığını unutmayın; bu da qstr.i.last dosyasının yalnızca son derlemeden bu yana değişmiş dosyalardan gelen verileri içereceği anlamına gelir.

  2. qstr.split, qstr.i.last üzerinde makeqstrdefs.py split çalıştırıldıktan sonra oluşturulan boş bir dosyadır. Yalnızca adımın çalıştığını belirtmek için bir bağımlılık olarak kullanılır. Bu betik, her giriş C dosyası için yalnızca eşleşen QSTR’leri içeren bir genhdr/qstr/...file.c.qstr dosyası üretir. Her QSTR Q(Foo) olarak yazdırılır. Bu adım, mevcut dosyaları qstr.i.last içindeki artımlı güncellemeden üretilen yeni verilerle birleştirmek için gereklidir.

  3. qstrdefs.collected.h, makeqstrdefs.py cat kullanılarak genhdr/qstr/* dosyalarının birleştirilmesinin çıktısıdır. Bu artık kodda bulunan, Q(Foo) biçiminde, satır başına bir tane ve yinelemelerle birlikte MP_QSTR_Foo‘ların tam kümesidir. Bu dosya yalnızca qstr kümesi değiştiğinde güncellenir. QSTR verisinin bir hash’i, derlemeler arasında değişiklikleri izlemeyi sağlayan başka bir dosyaya (qstrdefs.collected.h.hash) yazılır.

  4. Her girişi bir MP_QSTR_Foo‘yu karşılık gelen indeksiyle eşleyen bir numaralandırma (enumeration) üret. Bu, qstrdefs.collected.h ile qstrdefs*.h dosyalarını birleştirir, ardından her satırı Q(Foo) biçiminden "Q(Foo)" biçimine dönüştürür ki ön işlemciden değişmeden geçsinler. Sonra qstrdefs*.h içindeki herhangi bir koşullu derlemeyi ele almak için ön işlemci kullanılır. Daha sonra dönüşüm geri alınarak Q(Foo) haline getirilir ve qstrdefs.preprocessed.h olarak kaydedilir.

  5. qstrdefs.generated.h, makeqstrdata.py çıktısıdır. qstrdefs.preprocessed.h içindeki her Q(Foo) için (ve bazı ek sabit kodlanmış olanlar için), QDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo") çıktısını verir.

Ardından ana derlemede, qstrdefs.generated.h ile iki şey olur:

  1. qstr.h içinde, her QDEF bir enum’da bir giriş haline gelir; bu da MP_QSTR_Foo‘yu kodun kullanımına sunar ve o dizenin QSTR tablosundaki indeksine eşit kılar.

  2. qstr.c içinde, gerçek QSTR veri tablosu mp_qstr_const_pool->qstrs elemanları olarak üretilir.

Çalışma zamanı QSTR üretimi

Çalışma zamanında ek QSTR havuzları oluşturulabilir, böylece bunlara dize eklenebilir. Örneğin, şu kod:

foo[x] = 3

x değeri için bir QSTR oluşturulması gerekir ki “load attr” bytecode’u tarafından kullanılabilsin.

Ayrıca, Python kodu derlenirken tanımlayıcılar ve sabit değerler (literal) için QSTR’lerin oluşturulması gerekir. Not: yalnızca 10 karakterden kısa sabit değerler QSTR olur. Bunun nedeni, yığın (heap) üzerindeki normal bir dizenin her zaman en az 16 bayt (bir GC bloğu) yer kaplaması, oysa QSTR’lerin havuza daha verimli bir şekilde paketlenmelerine olanak tanımasıdır.

QSTR havuzları (ve dize verilerini depolayan alttaki “chunk”lar) yığın (heap) üzerinde minimum bir boyutla istek üzerine ayrılır.