Інтернування рядків у MicroPython

MicroPython використовує інтернування рядків для економії як RAM, так і ROM. Це дозволяє уникнути зберігання дублікатів одного і того ж рядка. Насамперед це стосується ідентифікаторів у коді, оскільки такі елементи, як назви функцій або змінних, дуже ймовірно зустрічаються в коді кілька разів. У MicroPython інтернований рядок називається QSTR (uniQue STRing).

Значення QSTR (типу qstr) — це індекс у зв’язаному списку пулів QSTR. QSTR зберігають свою довжину та хеш вмісту для швидкого порівняння під час процесу дедублікації. Усі операції байт-коду, що працюють з рядками, використовують аргумент QSTR.

Генерація QSTR під час компіляції

У коді C MicroPython будь-які рядки, що мають бути інтерновані у фінальній мікропрограмі, записуються як MP_QSTR_Foo. Під час компіляції це обчислюється у значення qstr, що вказує на індекс "Foo" у пулі QSTR.

Цей процес реалізується за допомогою багатоетапного механізму у Makefile. Коротко, він складається з трьох частин:

  1. Знайти всі токени MP_QSTR_Foo у коді.

  2. Згенерувати статичний пул QSTR, що містить усі рядкові дані (включно з довжинами та хешами).

  3. Замінити всі MP_QSTR_Foo (через препроцесор) відповідними індексами.

Токени MP_QSTR_Foo шукаються у двох джерелах:

  1. Усі файли, зазначені у $(SRC_QSTR). Це весь код C (тобто py, extmod, ports/stm32), але без коду сторонніх бібліотек, таких як lib.

  2. Додаткові $(QSTR_GLOBAL_DEPENDENCIES) (що включають mpconfig*.h).

Примітка: frozen_mpy.c (згенерований mpy-tool.py) має власну генерацію та пул QSTR.

Деякі додаткові рядки, які не можна виразити за допомогою синтаксису MP_QSTR_Foo (наприклад, ті, що містять неалфавітно-цифрові символи), явно надаються у qstrdefs.h та qstrdefsport.h через змінну $(QSTR_DEFS).

Обробка відбувається на таких етапах:

  1. qstr.i.last — це конкатенація результатів обробки кожного вхідного файлу через препроцесор C. Це означає, що будь-який умовно вимкнений код буде видалено, а макроси розгорнуто. Таким чином до пулу не потраплять рядки, які не використовуватимуться у фінальній мікропрограмі. Оскільки на цьому етапі (завдяки макросу NO_QSTR, доданому QSTR_GEN_CFLAGS) немає визначення для MP_QSTR_Foo, він проходить цей етап без змін. Цей файл також містить коментарі препроцесора з інформацією про номери рядків. Зауважте, що на цьому кроці обробляються лише змінені файли, тому qstr.i.last міститиме дані лише з файлів, що змінилися з моменту останньої компіляції.

  2. qstr.split — порожній файл, створений після запуску makeqstrdefs.py split для qstr.i.last. Він використовується лише як залежність для позначення виконання кроку. Цей скрипт виводить по одному файлу на кожен вхідний файл C: genhdr/qstr/...file.c.qstr, що містить лише знайдені QSTR. Кожен QSTR виводиться як Q(Foo). Цей крок необхідний для об’єднання наявних файлів з новими даними, отриманими при інкрементальному оновленні qstr.i.last.

  3. qstrdefs.collected.h — результат конкатенації genhdr/qstr/* за допомогою makeqstrdefs.py cat. Це повний набір знайдених у коді MP_QSTR_Foo, відформатованих як Q(Foo), по одному на рядок, з дублікатами. Цей файл оновлюється лише при зміні набору qstr. Хеш даних QSTR записується до окремого файлу (qstrdefs.collected.h.hash), що дозволяє відстежувати зміни між збірками.

  4. Генерується перелічення, кожен елемент якого відображає MP_QSTR_Foo на відповідний індекс. Відбувається конкатенація qstrdefs.collected.h з qstrdefs*.h, потім кожен рядок трансформується з Q(Foo) у "Q(Foo)" для незмінного проходження через препроцесор. Далі препроцесор обробляє умовну компіляцію у qstrdefs*.h. Після цього трансформація скасовується назад до Q(Foo) і результат зберігається як qstrdefs.preprocessed.h.

  5. qstrdefs.generated.h — результат роботи makeqstrdata.py. Для кожного Q(Foo) у qstrdefs.preprocessed.h (плюс деякі жорстко закодовані) виводиться QDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").

Потім під час основної компіляції з qstrdefs.generated.h відбуваються дві речі:

  1. У qstr.h кожен QDEF стає елементом перелічення, що робить MP_QSTR_Foo доступним у коді і рівним індексу цього рядка у таблиці QSTR.

  2. У 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 (та базові «чанки», що зберігають рядкові дані) виділяються на вимогу у купі з мінімальним розміром.