В обработчике бинарных данных разберите причину лишних копирований и выберите замену, уменьшающую пиковое потребление памяти при последовательной обработке срезов:
def consume(packet):
pass
def handle(data, ranges):
for start, end in ranges:
packet = data[start:end]
consume(packet)
payload = b"x" * 10_000_000
handle(payload, [(0, 1_000_000), (1_000_000, 2_000_000)])
Для объекта bytes выражение data[start:end] создаёт новый объект и копирует в него выбранные байты. Если обработчик не сохраняет пакет, срез следует заменить на memoryview(data)[start:end]: это представление участка исходного буфера без копирования.
Такая оптимизация уменьшает число аллокаций и пиковое потребление памяти, но требует, чтобы consume корректно работала с объектом, поддерживающим буферный протокол, и не удерживала большой исходный буфер дольше необходимого.
В Python есть буферный протокол — общий интерфейс для работы с непрерывными областями памяти без обязательного создания промежуточных объектов. Он нужен для эффективного обмена бинарными данными между встроенными типами и библиотеками, например при обработке файлов, сетевых пакетов и массивов.
Обычные операции над неизменяемыми последовательностями сохраняют простую семантику: результат среза является самостоятельным объектом. Для задач, где важна производительность, memoryview предоставляет отдельный явный выбор в пользу представления существующей памяти.
payload занимает примерно 10 МБ. Каждый срез bytes создаёт отдельный объект с копией выбранного диапазона; при больших пакетах, большом количестве итераций или одновременной обработке нескольких пакетов это увеличивает число аллокаций и временное потребление памяти.
Даже если временный пакет быстро уничтожается, копирование уже произошло и нагрузило CPU и аллокатор. Неверная замена на memoryview тоже может ухудшить ситуацию: маленькое представление удерживает ссылку на весь исходный буфер, поэтому большой bytes нельзя будет освободить, пока жив хотя бы один его view.
У bytes срез materialизуется как новый bytes:
Срез memoryview создаёт небольшой объект-представление с границами и метаданными, но не копирует байты. consume получает доступ к той же памяти через буферный протокол. Если функции требуется именно bytes, вызов bytes(packet) снова выполнит копирование, поэтому оптимизация действует только до места, где копия действительно необходима.
memoryview поддерживает не только bytes, но и другие буферы, включая bytearray и некоторые объекты массивов. Для операций, требующих изменяемости, нужно учитывать режим исходного буфера: представление над bytes доступно только для чтения, а представление над bytearray может быть изменяемым.
Главный компромисс — время жизни памяти. View удерживает исходный объект, поэтому для небольшого долгоживущего фрагмента большого входного буфера иногда выгоднее один раз скопировать данные в компактный bytes. Также не всякий внешний API принимает memoryview; перед интеграцией нужно проверить контракт функции и измерить эффект профилированием.
Сервис принимал большие сообщения, выделял из них заголовки и полезные данные, а затем последовательно вычислял контрольные суммы. Профилирование показало высокую частоту выделений и заметное время CPU в копировании, хотя одновременно в памяти находился только один пакет.
Рассматривались два варианта. Уменьшение размера входного буфера снизило бы память, но потребовало бы изменить протокол чтения и усложнило обработку границ сообщений. Простая замена bytes на bytearray не устраняла копирование срезов: обычный срез bytearray также создаёт новый объект.
Выбрали memoryview на время последовательной обработки и оставили преобразование в bytes только для сторонней функции, которая требовала неизменяемый объект. Это убрало копии на внутренних этапах и снизило пиковое потребление памяти; сохранённые результаты копировались явно, поэтому исходный сетевой буфер не удерживался дольше жизненного цикла запроса.
memoryview в список вместо немедленной обработки?Каждый view будет удерживать ссылку на исходный data. Поэтому список небольших представлений может сохранять в памяти весь большой входной буфер. Если нужны независимые результаты, следует явно создать копии нужного размера, например bytes(view), либо организовать чтение входа небольшими блоками.
memoryview не гарантирует ускорение любой обработки?Представление устраняет копирование при создании среза, но не делает последующие вычисления бесплатными. Если consume поэлементно выполняет медленный Python-код, стоимость обработки может доминировать. Кроме того, часть API преобразует view во внутренний буфер или требует bytes, и тогда копирование всё равно произойдёт.
memoryview?Для изменяемого буфера, например bytearray, содержимое view меняется вместе с источником. Обработчик может получить данные, отличающиеся от тех, которые были прочитаны в начале, а одновременное изменение размера исходного объекта может привести к ошибке или сделать операцию небезопасной. Поэтому при конкурентной обработке нужно обеспечить неизменность диапазона на время чтения либо передать независимую копию.