Learning Platform
Глоссарий Troubleshooting
Урок 08.01 · 30 мин
Продвинутый
Apache ArrowMemory LayoutValidity BitmapColumnar FormatZero-CopyBuffer Alignment

Memory Layout

Зачем свой формат в памяти

Традиционные структуры данных (ArrayList в Java, Vec в Rust, list в Python) хранят объекты поштучно — каждый элемент может лежать в произвольном месте кучи. Для аналитики это катастрофа: SIMD-инструкции процессора работают с непрерывными блоками, а кэш-линейка (64 байта) загружает мусор вместо полезных данных.

Apache Arrow определяет формат колоночных данных в памяти — фиксированную раскладку буферов, одинаковую для любого языка и рантайма. Один и тот же массив Arrow можно передать из Python в Rust без сериализации — просто через указатель.

TIP

Arrow — это не формат файлов. Это стандарт представления данных в оперативной памяти. IPC и Feather — сериализация этого представления на диск или в сеть, но каноническая форма Arrow живёт в RAM.

Validity Bitmap

Любой Arrow-массив может содержать NULL-значения. Вместо sentinel-значений (NaN, -1, пустая строка) Arrow использует отдельный validity bitmap — по одному биту на каждый элемент массива.

Правила:

  • Бит 1 — значение валидно, бит 0 — NULL
  • Биты упакованы least-significant bit first (LSB): бит 0 элемента i находится в байте i / 8, позиция i % 8
  • Размер bitmap: ceil(N / 8) байт, дополненный нулями до кратности 8 байт
  • Если массив не содержит NULL — bitmap может отсутствовать (null_count = 0)
Validity Bitmap: побитовая раскладка
Логические значенияМассив из 8 элементов. Элементы 2 и 5 — NULL, остальные содержат данные.
Validity Bitmap (1 байт)LSB-first: бит 0 = элемент 0, бит 7 = элемент 7. NULL на позициях 2 и 5 → биты 2 и 5 сброшены в 0.
1Бит 0 = 1 → элемент 0 (значение 10) валиден
1Бит 1 = 1 → элемент 1 (значение 20) валиден
0Бит 2 = 0 → элемент 2 — NULL. Соответствующие байты в буфере данных undefined.
1Бит 3 = 1 → элемент 3 (значение 40) валиден
1Бит 4 = 1 → элемент 4 (значение 50) валиден
0Бит 5 = 0 → элемент 5 — NULL. Данные в этой позиции не определены.
1Бит 6 = 1 → элемент 6 (значение 70) валиден
1Бит 7 = 1 → элемент 7 (значение 80) валиден

Бит 0 (LSB) ←————————————————→ Бит 7 (MSB)

Проверка NULL для элемента i:

is_valid = (bitmap[i / 8] >> (i % 8)) & 1

Операция O(1) — никакого сканирования, никаких хэш-таблиц. Один сдвиг и одно побитовое AND.

Массивы фиксированной ширины

Простейший случай — примитивные типы: Int32, Int64, Float64, Boolean. Каждый элемент занимает одинаковое количество байт.

Буферная раскладка массива Int32:

  1. Validity bitmapceil(N / 8) байт, padded до 64 байт
  2. Data bufferN × 4 байт (4 байта на Int32), padded до 64 байт
Int32 Array: буферная раскладка
Массив5 элементов типа Int32. Элемент 2 — NULL. Общий размер данных: 5 × 4 = 20 байт.
Buffer 0: Validity Bitmap1 байт данных (ceil(5/8)=1), дополнен до 64 байт нулями. Значение: 0b00011011 = 0x1B. Бит 2 = 0 → NULL.
Buffer 1: Data (Int32 × 5)20 байт данных (5 × 4), дополнено до 64 байт. Little-endian. Байты на позиции NULL (индекс 2) — undefined.

Доступ к элементу i:

value = *(int32_t*)(data_buffer + i * 4)

O(1) — прямое вычисление адреса. Нет индексов, нет B-деревьев, нет хэш-таблиц. Процессор загружает одну кэш-линейку (64 байта = 16 элементов Int32) — соседние элементы уже в кэше.

Массивы переменной ширины

Строки и бинарные данные не имеют фиксированного размера. Arrow использует три буфера:

  1. Validity bitmap — как для фиксированных типов
  2. Offsets buffer — массив из N + 1 смещений (Int32 для Utf8/Binary, Int64 для LargeUtf8/LargeBinary)
  3. Data buffer — конкатенация всех значений без разделителей

Значение элемента i — это срез data[offsets[i]..offsets[i+1]]. Длина = offsets[i+1] - offsets[i].

Utf8 Array: три буфера
Массив строк4 строки. Элемент 1 — NULL. Общий размер данных: 3 + 0 + 5 + 6 = 14 байт.
Buffer 0: Validity1 байт: 0b00001101 = 0x0D. Бит 1 = 0 → элемент 1 NULL.
Buffer 1: Offsets (Int32 × 5)N+1 = 5 значений. offsets[0]=0, offsets[1]=3 (после 'cat'), offsets[2]=3 (NULL — длина 0), offsets[3]=8 (после 'arrow'), offsets[4]=14 (после 'memory').
Buffer 2: Data (UTF-8 байты)Конкатенация: 'catarrowmemory'. Нет разделителей, нет терминаторов. Границы определяются только через offsets.
NOTE

Offset для NULL-элемента равен offset следующего элемента — длина строки 0. Байты в data buffer не определены (не обязательно пустые). Всегда проверяйте validity bitmap перед чтением.

Выравнивание и padding

Arrow требует выравнивания каждого буфера по 64 байтам. Это не произвольное число:

  • Кэш-линейка x86/ARM: 64 байта
  • AVX-512 регистр: 64 байта
  • Выравнивание устраняет unaligned access penalties и позволяет SIMD обрабатывать данные без копирования

Правила padding:

  • Начало каждого буфера выровнено по 64 байтам
  • Конец каждого буфера дополнен нулями до кратности 64 байт
  • Между буферами — padding до следующей 64-байтовой границы
Выравнивание буферов в памяти

Base Address (aligned to 64B)

Адрес начала массива. Выровнен по границе 64 байт (младшие 6 бит адреса = 0).
Validity Bitmap (1 байт данных + 63 байта padding)Даже для 1 байта bitmap — буфер занимает полные 64 байта. Padding заполнен нулями.
Data Buffer (20 байт данных + 44 байта padding)5 элементов Int32 = 20 байт. Дополнено до 64 байт. SIMD может обработать все 16 слотов за одну операцию.

Next Buffer (aligned to 64B)

Следующий буфер начинается ровно на границе 64 байт. Никаких промежутков.
WARNING

Arrow спецификация рекомендует 64-байтовое выравнивание (и большинство имплементаций следуют этому), но минимальное требование — 8 байт. Если вы пишете собственный producer — используйте 64 байта. Если читаете чужие данные — будьте готовы к 8-байтовому выравниванию.

O(1) Random Access

Вся конструкция Arrow оптимизирована под одну операцию: прямой доступ к элементу по индексу за O(1).

Для фиксированного типа (Int32):

address = base + buffer_offset + i * element_size

Для переменного типа (Utf8):

start = offsets[i]
end = offsets[i + 1]
value = data[start..end]

Никаких linked list, B-tree, hash table. Два умножения и одно сложение — даже без branch prediction miss.

Сравнение с другими форматами:

ФорматRandom AccessСканированиеNULL-проверка
ArrowO(1) — pointer arithmeticSIMD-friendly, 64B alignedO(1) — bitmap bit check
ParquetO(page) — нужно декодировать страницуТребует decompress + decodeRepetition/Definition levels
JSONO(N) — линейный поискСериализация/десериализацияПроверка типа значения
CSVO(N) — поиск разделителяПарсинг каждой строкиСравнение со строкой-маркером

Boolean Array: особый случай

Boolean в Arrow — не массив байтов. Каждое значение занимает 1 бит, упакованный так же, как validity bitmap (LSB-first).

Буферная раскладка:

  1. Validity bitmapceil(N / 8) байт
  2. Data bufferceil(N / 8) байт (значения, не байты)
Array: [true, false, true, true, NULL, false, true, true]

Validity: 0b 1110_1111 = 0xEF (бит 4 = 0 → NULL)
Data: 0b 1110_1101 = 0xED (бит 4 undefined)
TIP

Boolean массив из 1 миллиона элементов: 125 KB данных + 125 KB bitmap = 250 KB. Тот же массив в Java (boolean[]) — 1 MB (1 байт на элемент). Arrow экономит 4× памяти на boolean-колонках.

Ключевые выводы

  1. Validity bitmap хранит NULL-маску по 1 биту на элемент (LSB-first), проверка NULL — O(1) побитовая операция
  2. Фиксированная ширина (Int32, Float64) — один data buffer, доступ за O(1) через base + i × size
  3. Переменная ширина (Utf8, Binary) — offsets buffer (N+1 элементов) + data buffer, доступ за O(1) через data[offsets[i]..offsets[i+1]]
  4. 64-байтовое выравнивание обеспечивает SIMD-совместимость и совпадение с кэш-линейкой процессора
  5. Boolean упакованы по 1 биту — не 1 байт, что даёт 8× плотность хранения
DataFusion: Arrow memory layout deep-dive Python: PyArrow и Arrow в pandas/Polars

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Arrow-массив Int32 содержит 5 элементов: [100, 200, NULL, 400, 500]. Как проверить, что элемент с индексом 2 — NULL?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7