2025, Sep 16 10:01

Как быстро сериализовать float-эмбеддинги в Python: str(), json.dumps или orjson

Быстрый бенчмарк сериализации float-эмбеддингов в Python: str()/repr(), json.dumps и orjson. Почему orjson быстрее и что выбрать на практике. Тесты на float.

Преобразование больших списков вещественных эмбеддингов в строки может оказаться неожиданно медленным. Когда приходится записывать тысячи векторов в обычный текст, шаг сериализации становится заметным узким местом. Быстрый замер по распространённым подходам даёт любопытный результат: сторонняя библиотека orjson опережает встроенные варианты. Отсюда напрашивается вопрос — есть ли в стандартной библиотеке способ, который сравним с ней по скорости?

Пример: превращаем массив float в строки

import json, orjson
vals = [3.141592653589793] * 100
plain_repr = str(vals)          # Самый быстрый встроенный вариант для обычной строки
json_repr = json.dumps(vals)    # Соответствует JSON, но медленнее
fast_serial = orjson.dumps(vals) # Сторонняя библиотека, в целом самая быстрая

Что здесь действительно медленно

Если вы хотите получить в чистом Python производительность, близкую к orjson, встроенного аналога нет. orjson — это модуль-расширение на Rust, выполняющий сильно оптимизированную сериализацию на скорости C/Rust. Варианты стандартной библиотеки — str(), format(), json.dumps() — держат более строгие гарантии, и эта цена проявляется в бенчмарках.

Есть ещё один важный нюанс. и str(vals), и "{}".format(vals) опираются на один и тот же механизм (list.__str__), поэтому и работают почти одинаково. json.dumps() медленнее, потому что обязан соблюдать полную семантику JSON: экранирование, корректность вывода, точное обращение с числовыми типами и так далее.

Внешние библиотеки быстрее не вопреки своей «внешности», а потому что могут использовать иные реализации, не ломая совместимость. Преобразование чисел с плавающей запятой в строки нетривиально. Обсуждение, на которое вы ссылаетесь, касается замены существующего C-кода новыми и более быстрыми алгоритмами вроде Rye и Dragonbox. Возможно, orjson использует и другие механизмы управления строками. Выделение и освобождение памяти — дорогая операция, поэтому быстрые сериализаторы предварительно выделяют и переиспользуют буферы.

Есть и реальность микробенчмарков. Ваши цифры показывают, что str(x) менее чем вдвое медленнее orjson, тогда как в 2022 году отставание было 3–4 раза; либо улучшилась реализация, либо микробенчмарк слишком чувствителен к шуму. Оба тезиса отмечены и в обсуждении по ссылке.

Что использовать на практике

Если строгий JSON не нужен и важна просто читаемая строка, самый быстрый встроенный способ — str() или repr(). Они реализованы на C и это примерно максимум скорости в рамках стандартной библиотеки.

Если требуется JSON-совместимый вывод и важна скорость, orjson разработан под эту задачу и выигрывает у json и ujson в бенчмарках. Это на сегодня практичный выбор.

Но есть пара оговорок. Во-первых, json.dumps(x) != orjson.dumps(x). Считайте их разными инструментами, а не взаимозаменяемыми. Во-вторых, исходное измерение использовало целые числа, тогда как описанная нагрузка — про числа с плавающей запятой. Если ваши данные действительно float, меряйте на float, чтобы отражать реальный конвейер.

Есть и другой ракурс: сериализация в байты вместо строк. Поможет это или нет, зависит от требований следующего этапа; измеряйте на ваших данных и целевых системах, чтобы принять верный компромисс.

Зачем это важно

При сохранении множества эмбеддингов стоимость преобразования float-в-строку накапливается. Каждый лишний микросекунд на вектор в масштабе превращается в секунды и минуты. Выбор подхода в соответствии с требованиями к выходному формату — простая строка или JSON — избавляет от переплаты за ненужные гарантии и экономит время на горячем участке.

Вывод

Для обычного текстового представления используйте str() или repr() — и двигайтесь дальше. Для JSON-совместимого вывода с высокой пропускной способностью самый быстрый вариант — orjson.dumps, и в стандартной библиотеке сейчас равных ему нет. Не предполагаете равенства между json.dumps и orjson.dumps; уточняйте ожидания потребителей. И обязательно меряйте на реальных наборах float, по возможности с помощью pyperf, помня, что микробенчмарки шумные — прогоняйте достаточно итераций, чтобы получить стабильный сигнал.

Материал основан на вопросе на StackOverflow от K_Augus и ответе TAHSEEN BAIRAGDAR.