Почему msgspec такой быстрый?
Несколько дней назад я решил разобраться в устройстве msgspec. Получилось како бычно: я напал на него со своими PRами, мне через день выдали права на merge и release. Но самое главное: теперь я могу рассказать вам про внутреннее устройство самого быстрого сериализатора для json в питоне.
Как быстро распарсить json?
Традиционные парсеры json делают так: - Парсим весь json документ - Используем промежуточный слой для хранения json как примитивных Python объектов: dict, list, int, str, None, тд - Превращаем Python объекты в финальный вариант: датаклассы, модели, более сложные типы, тд
msgspec использует несколько важных хитростей, чтобы парсить json наиболее быстрым способом. Пример:
>>> import msgspec >>> class User(msgspec.Struct): ... username: str ... email: str ... >>> decoder = msgspec.json.Decoder(User) >>> decoder.decode(b'{"username": "example", "email": "[email protected]"}') User(username='example', email='[email protected]')
Все самое интересное происходит в JSONDecoder_decode и в json_decode: 1. Мы используем TypeNode *type для мета информации о том, что мы будем парсить. В нашем случае там будет struct User с двумя str полями 2. Далее мы проваливаемся в функцию json_decode_nocustom, она очень красивая:
static MS_INLINE PyObject * json_decode_nocustom( JSONDecoderState *self, TypeNode *type, PathNode *path ) { // ... switch (c) { case 'n': return json_decode_none(self, type, path); case 't': return json_decode_true(self, type, path); case 'f': return json_decode_false(self, type, path); case '[': return json_decode_array(self, type, path); case '{': return json_decode_object(self, type, path); case '"': return json_decode_string(self, type, path); default: return json_maybe_decode_number(self, type, path); } }
Буквально по первому символу, мы можем парсить нужные части. Хитрый json_decode_object посмотрит, что type у нас MS_TYPE_STRUCT и будет парсить сразу msgspec.Struct. Что еще более хитро, то парситься будут только те ключи, которые явно указаны в User, остальные будут просто пропускаться через вызов json_skip.
То есть: ключ в C мы конечно обязаны прочитать в виде char *, чтобы сравнить его с существующими ключами User. Но вот создавать дорогие промежуточные Python объекты мы не будем. Если ключ нам не нужен, то и значение его мы парсить не будем. На выходе получим сразу объект User без промежуточных слоев и их аллокаций. Быстро? Быстро.
Минусы
На данный момент у msgspec есть главный минус: плохая поддержка Union типов. То есть: некоторые комбинации данных вообще не получится распарсить. Например: str | bytes. Или два датакласса. Или два тайпдикта. Почему? Потому что оптимизации пока мешают работе 🌚 Но, вопрос решаем. Сделаем.
Второй минус: мало всего можно выразить. pydantic умеет куда больше. Потому я в django-modern-rest и сделал выбор сериализатора для каждого отдельного контроллера. Чтобы точечно выбирать скорость vs функциональность.
Что будет с msgspec дальше?
Новые релизы добавят кучу новых фичей. Поддержку pyrefly, heap types, поддержку subinterpreters, FT, более гибкие правила проверок значений и тд. А еще я параллельно добавил поддержку frozendict для Python 3.15+ и предложил сделать новое АПИ для него: PyFrozenDict_FromDictSteal, потому что текущее АПИ работает за O(n * 2), когда можно за O(n).
Обсуждение: а вы пробовали msgspec? Какие впечатления?
| Поддержать | YouTube | GitHub | Чат |