Массив из 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 для аналитики либо вынесение элементов в отдельную таблицу с индексами.
- Сжатие случайных данных без повторов увеличивает файл. Решение: оценивать энтропию на реальном наборе до включения кодека.
- Запись без журналирования на файловую систему, которая его не поддерживает: сбой питания в момент записи повреждает файл. Решение: журналируемая файловая система плюс запись через временный файл и атомарное переименование.
Перед выкладкой в продакшен прогоните один и тот же массив по маршруту запись - чтение на двух разных архитектурах и сверьте контрольную сумму. Такой тест занимает минуты и закрывает класс ошибок, которые иначе обнаруживаются уже на боевых данных.