Интернирование строк в MicroPython¶
MicroPython использует string interning для экономии как 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. Примечание: только литералы короче 10 символов становятся QSTR. Это связано с тем, что обычная строка в куче всегда занимает минимум 16 байт (один блок GC), тогда как QSTR позволяют упаковать их в пул более эффективно.
Пулы QSTR (и лежащие в их основе «чанки», хранящие строковые данные) выделяются в куче по мере необходимости с минимальным размером.