Интерактивный Arrow Memory Layout Viewer
Зачем нужен Memory Layout Viewer
В предыдущих уроках мы изучили каждый слой отдельно: fixed-width буферы, variable-width offsets, validity bitmaps, вложенные типы, IPC format. Теперь соберём всё вместе — в интерактивном viewer, который показывает реальную побайтовую структуру Arrow массивов в памяти.
Viewer ниже представляет 6 типов массивов с их буферами, byte offsets и bit-level деталями. Это то, что вы увидите при инспекции Arrow массива через pyarrow.Array.buffers() или при отладке через GDB/LLDB.
Выберите тип массива и кликайте на буферы, чтобы увидеть побайтовое содержимое.
Как читать viewer
Метаданные массива
Верхний блок — метаданные текущего массива:
- layout — физический тип размещения (Primitive, Variable-Width, Struct, Dictionary)
- length — количество элементов в массиве
- nulls — количество null-значений (null_count)
- buffers — количество буферов (validity + offsets/data + children)
- total — суммарный размер буферов (без учёта 64-byte padding)
Логический тип → семантика данных
Логический тип определяет семантику: Int32, Utf8, List<Int32>, Struct<name:Utf8, age:Int32>. Один логический тип может иметь разные физические layout (например, Utf8 vs Utf8View).Физический layout → количество буферов
Физический layout определяет количество и назначение буферов: Primitive = 1 data buffer, Variable-Width = offsets + data, Struct = только children. Layout фиксирован для каждого логического типа.Буферы → байты в памяти (64-byte aligned)
Буферы = непрерывные блоки памяти с 64-byte alignment. Каждый буфер имеет конкретное назначение: bitmap, offsets, values, child data. Все указатели + длины хранятся в ArrayData.Validity Bitmap
Каждый массив начинается с validity bitmap (фиолетовый буфер):
- Бит 1 = элемент определён (valid)
- Бит 0 = элемент null
- Биты упакованы LSB first: бит 0 байта 0 = элемент 0
- Если
null_count = 0, bitmap может быть опущен (указатель = NULL)
Переключитесь на Nullable Int32 в viewer, чтобы увидеть bitmap с тремя null-слотами.
Offsets и Data
Для variable-width типов (Utf8, Binary, List) viewer показывает два буфера:
- Offsets (синий) — массив int32/int64, где
string[i] = data[offsets[i]..offsets[i+1]] - Data (зелёный) — непрерывный буфер байтов всех значений
Переключитесь на Utf8 (String) — обратите внимание на N+1 offsets для N строк. Последний offset = размер всего data буфера. Это позволяет вычислить длину любой строки за O(1): len(s[i]) = offsets[i+1] - offsets[i].
Вложенные типы
List и Struct используют child arrays:
- List — offsets буфер + child array (элементы всех списков подряд)
- Struct — только validity bitmap + children (по одному на поле)
Переключитесь на Struct в viewer. Обратите внимание: struct с null в bitmap имеет undefined данные в child arrays — движок проверяет struct bitmap первым.
Dictionary
Dictionary хранит данные как индексы в словарь уникальных значений:
- Indices (синий) — int32 индексы в dictionary values array
- Dictionary Values (зелёный) — Utf8 массив уникальных строк
Переключитесь на Dictionary — при 8 элементах и 3 уникальных строках: 32 байта индексов + 28 байтов словаря = 60 байтов вместо ~50 байтов прямого хранения. Выигрыш становится существенным при миллионах строк.
Инспекция реального массива
В viewer мы используем предопределённые данные. В реальной работе вы будете инспектировать массивы через PyArrow:
import pyarrow as pa
# Создание массива
arr = pa.array([10, 20, None, 40, 50], type=pa.int32())
# Инспекция буферов
print(f"Type: {arr.type}") # int32
print(f"Length: {len(arr)}") # 5
print(f"Null count: {arr.null_count}") # 1
print(f"Buffers: {len(arr.buffers())}") # 2
# Побайтовый доступ
validity = arr.buffers()[0]
data = arr.buffers()[1]
print(f"Validity: {validity.hex()}") # 1b = 00011011
print(f"Data size: {data.size}") # 20 bytes (5 × 4)
# Для Utf8
names = pa.array(["Alice", "Боб", None])
bufs = names.buffers()
print(f"Offsets: {bufs[1].hex()}") # 3 × int32 offsets
print(f"Data: {bufs[2].hex()}") # UTF-8 bytes
array.buffers() возвращает список pyarrow.Buffer объектов. Для Primitive: [validity, data]. Для Utf8: [validity, offsets, data]. Для Struct: [validity] — child arrays доступны через array.field(i). Buffer.hex() показывает raw bytes — те самые байты, что viewer отображает в раскрытых секциях.
Паттерны инспекции
| Задача | Команда / API |
|---|---|
| Буферы массива | arr.buffers() |
| Null count | arr.null_count |
| Validity bitmap | arr.buffers()[0].hex() |
| Data buffer raw | arr.buffers()[1].hex() |
| RecordBatch buffers | batch.column(i).buffers() |
| Dictionary values | arr.dictionary |
| Dictionary indices | arr.indices |
| Общий размер | pa.total_allocated_bytes() |
Ключевые выводы
-
Каждый тип = фиксированный набор буферов. Int32 — 2 буфера (validity + data). Utf8 — 3 (validity + offsets + data). Struct — только validity + children. Viewer показывает это наглядно.
-
Null-слоты занимают место. В data buffer null-элемент имеет слот с undefined bytes. Это сохраняет O(1) random access:
data[i × sizeof]всегда валидный offset, значение проверяется по bitmap. -
Offsets = N+1 элементов. Для N строк нужен N+1 offset — последний указывает конец данных. Это позволяет вычислить длину любого элемента за O(1).
-
Children — полноценные arrays. В List и Struct child arrays имеют собственные буферы, bitmap, и тип. Структура рекурсивна:
List<Struct<name:Utf8, scores:List<Float64>>>= 4 уровня вложенности. -
Dictionary = indices + values. Выигрыш при low cardinality: 1M строк × 10 уникальных → 4MB indices + ~100B dict вместо ~10MB строк.