Сериализация и персистентность массивов на диске: форматы, порядок байтов и выравнивание | AdminWiki

Сериализация и персистентность массивов на диске: форматы, порядок байтов и выравнивание

22 сентября 2026 15 мин. чтения
Содержание статьи

Массив из 1000 целых чисел типа int32 занимает в оперативной памяти ровно 4000 байт. Столько же займёт бинарный файл с этим массивом, если записать его дампом памяти. Тот же массив в текстовом виде раздувается до 5 КБ и больше, а при чтении на машине с другим порядком байтов даст тысячу мусорных значений вместо исходных чисел.

Короткий ответ: чтобы массив корректно лёг на диск и вернулся оттуда без искажений, зафиксируйте четыре вещи. Формат сериализации выбирайте под задачу, будь то аналитика, обмен между сервисами или быстрый доступ к одному файлу. Порядок байтов задавайте явно, обычно через сетевой порядок big-endian. Выравнивание контролируйте, чтобы padding не попал в файл. Ограничения файловой системы проверяйте до записи: FAT32 не примет файл размером 4 ГБ и больше.

Дальше разбираем форматы (бинарный, JSON, MessagePack, Parquet), примеры кода на C и Python, хранение в SQLite и PostgreSQL, сжатие массивов и чек-лист ошибок, из-за которых данные портятся при переносе между системами.

Что такое сериализация массивов и зачем она нужна на диске

Сериализация - это преобразование структуры данных (массива, словаря, структуры) в последовательность байтов, которую можно записать в файл, отправить по сети или положить в колонку базы данных. Обратная операция называется десериализацией.

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

Разница между представлениями видна на цифрах. Массив из 1000 значений int32 в памяти занимает 4000 байт. Бинарная запись тем же дампом даёт файл 4000 байт. Текстовый формат с числами через запятую займёт 5-6 КБ, потому что каждое число превращается в несколько ASCII-символов. Если записать массив сырыми байтами на x86 и прочитать на big-endian системе, получится 1000 неверных значений, хотя длина файла совпадёт.

Игнорирование порядка байтов и выравнивания бьёт именно по переносимости: локально всё работает, а после миграции сервиса на другую архитектуру или после смены версии библиотеки данные становятся нечитаемыми. Проверка формата на тестовом наборе до продакшена дешевле, чем восстановление массива по частям.

Ключевые термины: сериализация, персистентность, порядок байтов, выравнивание

  • Сериализация - преобразование массива в поток байтов; десериализация - сборка массива обратно из байтов.
  • Персистентность - сохранение данных на диске с возможностью восстановления после перезапуска процесса или узла.
  • Порядок байтов (endianness) - порядок хранения байтов многобайтового числа. Little-endian пишет первым младший байт, big-endian - старший.
  • Выравнивание - размещение полей по адресам, кратным их размеру, чтобы процессор читал значение за одно обращение к памяти.

Число 0x12345678 в little-endian лежит в памяти как 78 56 34 12, в big-endian - как 12 34 56 78. Одно и то же значение, два разных набора байтов на диске.

Выравнивание добавляет в структуру служебные байты (padding). Структура из char и int на 32-битной платформе займёт 8 байт: 1 байт символа, 3 байта заполнения и 4 байта целого. Сумма полей равна 5 байтам, разницу даёт раскладка полей по границам. Как раскладка структур влияет на память и скорость, разобрано в материале о padding и выравнивании структур.

Обзор форматов сериализации массивов: бинарный, JSON, MessagePack, Parquet

Формат определяет три вещи: сколько места займёт массив, как быстро он записывается и читается, и насколько просто проверить данные глазами или прочитать их другим сервисом. Ниже четыре распространённых варианта и их поведение.

Бинарный формат: скорость и компактность

Бинарный формат записывает массив как непрерывный блок байтов. В C это один вызов fwrite для всего массива, в Python - одна операция struct.pack. Плюсы: минимальный размер (4000 байт для 1000 int32), максимальная скорость записи и чтения, нулевые расходы на разбор текста.

Минусы: файл нечитаем без знания структуры, метаданных внутри нет, а порядок байтов и выравнивание наследуются от машины, где шла запись. При чтении на другой платформе без преобразования порядка байтов значения искажаются. Практика: фиксируйте порядок байтов (сетевой big-endian либо явно little-endian), опишите формат в документе или схеме, структуры упаковывайте без padding.

JSON: универсальность и читаемость

Массив [1, 2, 3] в JSON читается человеком и разбирается любой библиотекой на любом языке. Формат не зависит от порядка байтов: числа записаны текстом, строки кодируются в UTF-8. Это делает JSON удобным для конфигураций, обмена между сервисами и отладки.

Ограничения проявляются на объёмах. Текстовая запись занимает больше места, парсинг требует разбора всей структуры, а двоичные данные приходится кодировать в Base64 с ростом размера примерно на треть. По RFC 8259 реализации вправе ограничивать диапазон и точность чисел; поскольку IEEE 754 binary64 (двойная точность) широко доступна, хорошая совместимость достигается для целых чисел в диапазоне от -(2^53)+1 до (2^53)-1 - именно в этих границах реализации согласуются по значению точно. Числа за пределами этого диапазона могут терять точность при прогоне через парсер. Для массивов в сотни мегабайт JSON не подходит.

MessagePack: бинарный JSON

MessagePack - это спецификация сериализации объектов, подобная JSON, но данные хранятся в бинарном виде. Целые числа пакуются в формат Int, который занимает 1, 2, 3, 5 или 9 байт, а массивы - в формат Array, который добавляет 1, 3 или 5 байт к элементам. Библиотеки есть для Python, Go, Java, C, Rust и JavaScript, что позволяет обмениваться данными между сервисами без потери структуры.

Минус один: файл нечитаем глазами, для отладки нужен отдельный декодер. MessagePack уместен для обмена сообщениями и кэша, где важны размер и скорость, а структура данных остаётся стабильной.

Parquet: колоночное хранение для аналитики

Parquet хранит данные по колонкам, а не по строкам. Для массива однотипных значений это даёт два эффекта: одинаковые типы сжимаются лучше, а при чтении одной колонки остальные не читаются с диска. Внутри Parquet поддерживает словарное кодирование (Dictionary Encoding, включая PLAIN_DICTIONARY = 2 и RLE_DICTIONARY = 8) и кодирование Run Length Encoding / Bit-Packing Hybrid (RLE = 3), которое сочетает битовую упаковку и кодирование длин серий для эффективного хранения повторяющихся значений.

Формат хранит схему и метаданные, поэтому данные читаются разными инструментами (pyarrow, Spark, DuckDB). Запись идёт блоками, по одному элементу дописывать нельзя. Для аналитических массивов с повторяющимися значениями Parquet даёт выигрыш по размеру в разы относительно текстовых форматов; точная цифра зависит от данных и выбранного кодека.

Таблица ниже показывает порядок величин. Оценки относительные: конкретные числа зависят от библиотеки, версии и характера массива, поэтому замеряйте на своих данных.

ФорматРазмерСкоростьЧитаемостьСхемаКогда выбирать
Бинарный (raw)МинимальныйМаксимальнаяНетВнешняя, задаётся вручнуюСвой формат, обмен внутри одного стека
JSONНаибольшийНизкая на объёмахПолнаяНеобязательнаяКонфиги, API, небольшие массивы
MessagePackСреднийВысокаяЧерез декодерНеобязательнаяОбмен между сервисами, кэш
ParquetМалый при сжатииВысокая на чтение колонокЧерез инструментыОбязательнаяАналитика, большие массивы на диске

Для обмена между сервисами чаще берут JSON или MessagePack, для больших массивов на диске - Parquet или бинарный формат с описанной схемой. Сравнение JSON, BLOB и бинарных форматов при хранении в одной колонке БД с цифрами по размеру и скорости собрано в материале о сериализации массивов в базу данных.

Порядок байтов (endianness): как не потерять данные при переносе

x86 и x86_64 работают в little-endian. Сетевые протоколы исторически используют big-endian, поэтому его называют сетевым порядком. ARM поддерживает оба режима, и конкретный выбор зависит от сборки и настроек платформы.

Little-endian vs big-endian: примеры и последствия

Число 0x0A0B0C0D занимает 4 байта. В little-endian на диске они идут как 0D 0C 0B 0A, в big-endian - как 0A 0B 0C 0D. Массив из 1000 int32, записанный на x86 сырыми байтами, на big-endian системе прочитается как 1000 других чисел, при этом размер файла совпадёт байт в байт, и ошибка не проявится как сбой чтения.

Проверка порядка байтов на месте занимает несколько строк:

#include <stdio.h>
int main(void) {
    unsigned int x = 1;
    unsigned char *p = (unsigned char *)&x;
    printf(p[0] ? "little-endian\n" : "big-endian\n");
    return 0;
}

Тот же эффект даёт запись числа как есть в бинарный файл. Решение одно: зафиксировать порядок байтов в спецификации формата и преобразовывать значения на границе записи и чтения.

Практические приёмы: htonl, ntohl и сетевой порядок

В C для 32- и 16-битных чисел есть функции htons, htonl, ntohs, ntohl. Перед записью каждый элемент массива прогоняется через htonl, при чтении - через ntohl. Для 64-битных значений на POSIX-системах доступны htobe64 и be64toh: они преобразуют байтовое представление целых между порядком байтов хоста и big-endian, а число в имени функции указывает размер обрабатываемого целого - 16, 32 или 64 бита. Проверьте их наличие в вашей библиотеке.

В Python порядок задаётся префиксом в строке формата: '>' означает big-endian, '<' - little-endian, '=' и '!' задают нативный и сетевой порядок. В Go используют encoding/binary с явным порядком (binary.BigEndian, binary.LittleEndian), в Java - ByteBuffer с методом order().

Текстовые форматы (JSON, CSV) этой проблемы не имеют, потому что записывают числа символами. Любой бинарный формат требует явного соглашения о порядке байтов, иначе кросс-платформенный обмен ломается.

Выравнивание данных: почему размер структуры больше суммы полей

Компиляторы размещают поля структур по адресам, кратным размеру поля, чтобы процессор читал значение за одно обращение к памяти. Многобайтовые типы получают смещения, кратные 2, 4 или 8, а между полями появляются байты заполнения.

Как выравнивание влияет на размер и производительность

Структура из char и int на 32-битной платформе: 1 байт символа, 3 байта padding (чтобы целое легло на смещение 4) и 4 байта значения. Итого 8 байт вместо 5. Массив из 1000 таких структур займёт 8000 байт, и 3000 лишних байтов попадут в файл, если писать структуры дампом памяти.

Порядок полей меняет размер. Раскладка «сначала многобайтовые поля, затем короткие» сокращает заполнение до минимума, а обратный порядок добавляет padding на каждой структуре. При сериализации массива структур на диск эти байты увеличивают файл и создают расхождение при чтении на другой платформе, где компилятор выравнивает иначе.

Обратная сторона: невыровненный доступ медленнее на x86 и может вызывать аппаратное исключение на архитектурах со строгим выравниванием, например на части ARM-платформ. Для работы в памяти выгоднее выровненная раскладка, для хранения на диске - плотная упаковка.

Упакованные структуры и pragma pack

Компилятору можно запретить вставку padding. В MSVC и GCC/Clang работает #pragma pack(push, 1) ... #pragma pack(pop), в GCC/Clang также __attribute__((packed)). Структура из char и int с такой директивой займёт ровно 5 байт, и массив в файле не будет содержать мусорных байтов.

Цена упаковки - невыровненный доступ. На x86 это обычно только замедление, на ARM может привести к ошибке шины. Безопасная альтернатива: не писать структуры целиком, а сериализовать поля по отдельности, каждое через свою функцию преобразования порядка байтов. Тогда формат на диске не зависит ни от раскладки структур в конкретном компиляторе, ни от платформы.

Практические примеры сохранения массивов в файлы и базы данных

Адрес любого элемента в непрерывном массиве считается как base + i * size, поэтому бинарная запись сводится к работе с одним блоком памяти. Как вычисляется адрес элемента, разобрано в материале об адресной арифметике в массивах. Такой блок удобно писать целиком и читать через mmap без копирования.

Сохранение массива в бинарный файл: примеры на C и Python

Запись массива int32 с преобразованием в сетевой порядок байтов на C:

#include <stdio.h>
#include <arpa/inet.h>

int arr[1000];
for (int i = 0; i < 1000; i++) arr[i] = htonl(arr[i]);

FILE *f = fopen("data.bin", "wb");
fwrite(arr, sizeof(int), 1000, f);
fclose(f);

Чтение выполняется зеркально: fread на весь массив, затем ntohl для каждого элемента. Для файлов, которые читаются часто, применяют mmap: файл отображается в адресное пространство, и данные доступны как обычный массив без чтения целиком. В Python отображение даёт модуль mmap, а разбор значений - struct.unpack_from.

import struct, mmap

data = struct.pack('>1000i', *arr)
with open('data.bin', 'wb') as f:
    f.write(data)

with open('data.bin', 'rb') as f:
    with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
        value = struct.unpack_from('>i', mm, 40)[0]

Префикс '>' в строках формата означает big-endian, поэтому файл читается одинаково на любой платформе.

Хранение массивов в базах данных: SQLite и PostgreSQL

В SQLite массив кладут в колонку типа BLOB, в PostgreSQL - в колонку bytea. Сериализация та же, что для файла, меняется только транспорт.

import sqlite3, struct

conn = sqlite3.connect('test.db')
conn.execute('CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, data BLOB)')
payload = struct.pack('>1000i', *arr)
conn.execute('INSERT INTO t (id, data) VALUES (?, ?)', (1, payload))
conn.commit()

В PostgreSQL бинарные данные передают через psycopg2.Binary, чтобы драйвер не применял текстовое экранирование:

import psycopg2
cur.execute('CREATE TABLE IF NOT EXISTS t (id int primary key, data bytea)')
cur.execute('INSERT INTO t (id, data) VALUES (%s, %s)', (1, psycopg2.Binary(payload)))

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

Использование Parquet для больших массивов

Запись аналитического массива в Parquet через pyarrow занимает несколько строк:

import pyarrow as pa
import pyarrow.parquet as pq

arr = pa.array(list(range(1000000)))
table = pa.Table.from_arrays([arr], names=['col'])
pq.write_table(table, 'data.parquet', compression='zstd')

Файл хранит схему и сжат блочно, поэтому читается выборочно и быстро. Дописывать по одному элементу нельзя: Parquet пишут блоками. Перед записью стоит оценить объём буферов: расчёт размера массива с учётом заголовков объектов, выравнивания и оверхеда аллокатора разобран в материале о профилировании памяти под массивы.

Сжатие данных при сериализации массивов

Сжатие уменьшает информационный объём за счёт устранения избыточности использованного кода. При сжатии без потерь исходный вид данных восстанавливается без искажений, и метод применим к любой информации, включая тексты, программы и числовые массивы. Сжатие с регулируемыми потерями отбрасывает часть данных, которая уже не восстанавливается, и используется для видео, звука и изображений. Описание методов сжатия и примеры расчётов

Сжатие без потерь и с потерями: что выбрать для массивов

Для массивов чисел, счётчиков, метрик и индексов берут методы без потерь: RLE, Huffman, LZ77, Deflate, LZ4, Zstandard. Parquet и большинство колоночных форматов включают такое сжатие внутри файла, поэтому отдельная упаковка поверх готового файла редко что-то добавляет.

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

Пример RLE и оценка коэффициента сжатия

RLE (Run Length Encoding) заменяет повторяющиеся последовательности данных структурой из счётчика повторений и кода данных. Последовательность 0 0 0 127 127 0 255 255 255 255 (10 байт) превращается в вектор 3 0 2 127 1 0 4 255 (8 байт), коэффициент сжатия равен 80%. Пример RLE и расчёт коэффициента

Обратное восстановление требует служебной информации: в сообщение включают заголовок, где каждому коду сопоставлен ASCII-код символа. Оценка на текстовых данных: файл из 10 Кбайт с четырьмя различными символами, закодированный двумя битами на символ плюс заголовок 5 байт, сжимается до 2565 байт, коэффициент примерно 4. Расчёт для файла с четырьмя символами Оба примера показывают механику на уровне байтовых последовательностей; для массива чисел коэффициент зависит от доли повторов в нём.

Влияние файловой системы на персистентность массивов

Формат файла решает, как байты уложены внутри, а файловая система определяет, какой размер файла допустим, что происходит при сбое питания и можно ли ограничить доступ к данным.

Ограничения FAT32: 4 ГБ на файл и отсутствие журналирования

Максимальный размер одного файла в FAT32 составляет 4 ГБ минус 1 байт. Ограничения и особенности FAT32 Для персистентности массивов это жёсткий предел: дамп сырых данных больше лимита записать не получится, даже если на диске свободны десятки гигабайт.

Штатные средства Windows позволяют форматировать в FAT32 тома только до 32 ГБ, хотя сама файловая система теоретически поддерживает значительно большие разделы. Права доступа к файлам и шифрование на уровне файловой системы отсутствуют: любой, кто получил накопитель, читает все данные. Журналирования нет, поэтому при внезапном отключении питания риск повреждения данных выше. Ограничение 32 ГБ и отсутствие журналирования

При активной записи и удалении файлы фрагментируются, что со временем замедляет чтение. Совместимость широкая: формат понимают Windows, macOS, Linux, телевизоры, магнитолы, приставки, фотоаппараты и медиаплееры. Для серверного хранения больших массивов выбирайте журналируемые файловые системы, а FAT32 оставьте для флешек и бытовой техники. Фрагментация и совместимость FAT32

Современные файловые системы: NTFS, ext4, ZFS

NTFS закрывает основные ограничения FAT32: журналирование, права доступа, шифрование и поддержку больших файлов. ext4 - рабочий стандарт для Linux с журналированием и большими томами. ZFS добавляет контрольные суммы блоков, снимки, клоны и сжатие на уровне файловой системы, поэтому молчаливое повреждение данных выявляется при чтении. Для массивов на серверах и NAS выбирайте журналируемую файловую систему, ZFS уместна там, где важна целостность. Набор возможностей зависит от версии и настроек, сверяйтесь с документацией вашего дистрибутива или сборки.

Типичные ошибки и как их избежать

Ошибки порядка байтов и выравнивания

  • Массив int32 записан на x86 сырыми байтами и прочитан на big-endian системе. Решение: преобразование через htonl и ntohl, сетевой порядок в спецификации формата.
  • Структура { char; int; } занимает 8 байт вместо 5, и 3 байта padding попали в файл. При чтении на другой платформе эти байты интерпретируются как данные. Решение: упакованные структуры (pragma pack) или сериализация полей по отдельности.
  • Формат описан только в коде, без версии. Решение: версионирование схемы и явные правила обратной совместимости, как в Protobuf и Avro, где новые поля добавляются с сохранением чтения старых записей.

Ошибки при работе с файловыми системами и базами данных

  • Массив записан в SQLite как BLOB или в PostgreSQL как bytea без фиксации порядка байтов, и на другой платформе распаковывается неверно. Решение: всегда писать в согласованном порядке байтов.
  • Попытка записать файл размером 4 ГБ и больше на FAT32 заканчивается ошибкой «Файл слишком велик для конечной файловой системы». Это следствие лимита файловой системы, а не неисправность накопителя. Решение: NTFS, ext4 или ZFS.
  • Поиск по элементам массива внутри BLOB без индексов: полный перебор на каждый запрос. Решение: Parquet для аналитики либо вынесение элементов в отдельную таблицу с индексами.
  • Сжатие случайных данных без повторов увеличивает файл. Решение: оценивать энтропию на реальном наборе до включения кодека.
  • Запись без журналирования на файловую систему, которая его не поддерживает: сбой питания в момент записи повреждает файл. Решение: журналируемая файловая система плюс запись через временный файл и атомарное переименование.

Перед выкладкой в продакшен прогоните один и тот же массив по маршруту запись - чтение на двух разных архитектурах и сверьте контрольную сумму. Такой тест занимает минуты и закрывает класс ошибок, которые иначе обнаруживаются уже на боевых данных.

Поделиться:
Сохранить гайд? В закладки браузера