Інтернування рядків у MicroPython¶
MicroPython використовує інтернування рядків для економії як RAM, так і ROM. Це дозволяє уникнути зберігання дублікатів одного і того ж рядка. Насамперед це стосується ідентифікаторів у коді, оскільки такі елементи, як назви функцій або змінних, дуже ймовірно зустрічаються в коді кілька разів. У MicroPython інтернований рядок називається QSTR (uniQue STRing).
Значення QSTR (типу qstr) — це індекс у зв’язаному списку пулів QSTR. QSTR зберігають свою довжину та хеш вмісту для швидкого порівняння під час процесу дедублікації. Усі операції байт-коду, що працюють з рядками, використовують аргумент QSTR.
Генерація QSTR під час компіляції¶
У коді C MicroPython будь-які рядки, що мають бути інтерновані у фінальній мікропрограмі, записуються як MP_QSTR_Foo. Під час компіляції це обчислюється у значення qstr, що вказує на індекс "Foo" у пулі QSTR.
Цей процес реалізується за допомогою багатоетапного механізму у Makefile. Коротко, він складається з трьох частин:
Знайти всі токени
MP_QSTR_Fooу коді.Згенерувати статичний пул QSTR, що містить усі рядкові дані (включно з довжинами та хешами).
Замінити всі
MP_QSTR_Foo(через препроцесор) відповідними індексами.
Токени MP_QSTR_Foo шукаються у двох джерелах:
Усі файли, зазначені у
$(SRC_QSTR). Це весь код C (тобтоpy,extmod,ports/stm32), але без коду сторонніх бібліотек, таких якlib.Додаткові
$(QSTR_GLOBAL_DEPENDENCIES)(що включаютьmpconfig*.h).
Примітка: frozen_mpy.c (згенерований mpy-tool.py) має власну генерацію та пул QSTR.
Деякі додаткові рядки, які не можна виразити за допомогою синтаксису MP_QSTR_Foo (наприклад, ті, що містять неалфавітно-цифрові символи), явно надаються у qstrdefs.h та qstrdefsport.h через змінну $(QSTR_DEFS).
Обробка відбувається на таких етапах:
qstr.i.last— це конкатенація результатів обробки кожного вхідного файлу через препроцесор C. Це означає, що будь-який умовно вимкнений код буде видалено, а макроси розгорнуто. Таким чином до пулу не потраплять рядки, які не використовуватимуться у фінальній мікропрограмі. Оскільки на цьому етапі (завдяки макросуNO_QSTR, доданомуQSTR_GEN_CFLAGS) немає визначення дляMP_QSTR_Foo, він проходить цей етап без змін. Цей файл також містить коментарі препроцесора з інформацією про номери рядків. Зауважте, що на цьому кроці обробляються лише змінені файли, томуqstr.i.lastміститиме дані лише з файлів, що змінилися з моменту останньої компіляції.qstr.split— порожній файл, створений після запускуmakeqstrdefs.py splitдля qstr.i.last. Він використовується лише як залежність для позначення виконання кроку. Цей скрипт виводить по одному файлу на кожен вхідний файл C:genhdr/qstr/...file.c.qstr, що містить лише знайдені QSTR. Кожен QSTR виводиться якQ(Foo). Цей крок необхідний для об’єднання наявних файлів з новими даними, отриманими при інкрементальному оновленніqstr.i.last.qstrdefs.collected.h— результат конкатенаціїgenhdr/qstr/*за допомогоюmakeqstrdefs.py cat. Це повний набір знайдених у кодіMP_QSTR_Foo, відформатованих якQ(Foo), по одному на рядок, з дублікатами. Цей файл оновлюється лише при зміні набору qstr. Хеш даних QSTR записується до окремого файлу (qstrdefs.collected.h.hash), що дозволяє відстежувати зміни між збірками.Генерується перелічення, кожен елемент якого відображає
MP_QSTR_Fooна відповідний індекс. Відбувається конкатенаціяqstrdefs.collected.hзqstrdefs*.h, потім кожен рядок трансформується зQ(Foo)у"Q(Foo)"для незмінного проходження через препроцесор. Далі препроцесор обробляє умовну компіляцію уqstrdefs*.h. Після цього трансформація скасовується назад доQ(Foo)і результат зберігається якqstrdefs.preprocessed.h.qstrdefs.generated.h— результат роботиmakeqstrdata.py. Для кожногоQ(Foo)у qstrdefs.preprocessed.h (плюс деякі жорстко закодовані) виводитьсяQDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").
Потім під час основної компіляції з qstrdefs.generated.h відбуваються дві речі:
У qstr.h кожен QDEF стає елементом перелічення, що робить
MP_QSTR_Fooдоступним у коді і рівним індексу цього рядка у таблиці QSTR.У qstr.c генерується фактична таблиця даних QSTR як елементи
mp_qstr_const_pool->qstrs.
Генерація QSTR під час виконання¶
Під час виконання можна створювати додаткові пули QSTR, до яких додаються рядки. Наприклад, такий код:
foo[x] = 3
Потребуватиме створення QSTR для значення x, щоб його можна було використати байт-кодом «load attr».
Крім того, під час компіляції коду Python ідентифікаторам і літералам потрібно присвоювати QSTR. Примітка: QSTRами стають лише літерали коротші за 10 символів. Це пов’язано з тим, що звичайний рядок у купі займає щонайменше 16 байт (один блок GC), тоді як QSTR дозволяє ефективніше упаковувати їх у пул.
Пули QSTR (та базові «чанки», що зберігають рядкові дані) виділяються на вимогу у купі з мінімальним розміром.