Learning Platform
Глоссарий Troubleshooting
Урок 08.07 · 25 мин
Средний
ArrowMemory LayoutInteractiveBuffersViewer

Интерактивный 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.

Выберите тип массива и кликайте на буферы, чтобы увидеть побайтовое содержимое.

Интерактивный Arrow Memory Layout Viewer
Выберите тип массива
Примитивный fixed-width массив. Один буфер данных, каждый элемент = 4 байта. O(1) доступ по индексу: offset = index × 4.Variable-width строковый массив. Offsets буфер (int32) указывает начало/конец каждой строки в data буфере.Int32 с null-значениями. Validity bitmap указывает, какие элементы определены. Null-слоты занимают место в data буфере (undefined bytes).Вложенный тип: массив массивов. Offsets буфер описывает границы внутренних списков, child array хранит все элементы подряд.Struct хранит поля как отдельные child arrays. Нет собственного data буфера — только validity bitmap + ссылки на children.Dictionary encoding: данные хранятся как индексы в словарь уникальных значений. Экономия памяти при low cardinality.
Int32Логический тип: Int32. Физический layout: Primitive (Fixed-Width)layout:Primitive (Fixed-Width)length:8nulls:0buffers:2total:40 B
Данные:[10, 20, 30, 40, 50, 60, 70, 80]
Распределение памяти
Validity Bitmap: 8 B (20%)
Data Buffer: 32 B (80%)
Validity Bitmap20%Data Buffer80%
Буферы (2) — кликните для раскрытия
Примитивный fixed-width массив. Один буфер данных, каждый элемент = 4 байта. O(1) доступ по индексу: offset = index × 4.
Буферы:● Validity Bitmap● Offsets● Data● Child (Utf8)

Как читать viewer

Метаданные массива

Верхний блок — метаданные текущего массива:

  • layout — физический тип размещения (Primitive, Variable-Width, Struct, Dictionary)
  • length — количество элементов в массиве
  • nulls — количество null-значений (null_count)
  • buffers — количество буферов (validity + offsets/data + children)
  • total — суммарный размер буферов (без учёта 64-byte padding)
Три уровня инспекции Arrow массива

Логический тип → семантика данных

Логический тип определяет семантику: 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 (зелёный) — непрерывный буфер байтов всех значений
TIP

Переключитесь на 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 байтов прямого хранения. Выигрыш становится существенным при миллионах строк.

Паттерны доступа к памяти по типам
Primitive (Int32, Float64)Один непрерывный буфер фиксированной ширины. Каждый элемент по предсказуемому offset = index × sizeof(type). Идеально для SIMD: загружаем 256-bit регистр = 8 int32 за одну инструкцию.
Variable-Width (Utf8, Binary)Два буфера: offsets (фиксированный) + data (переменный). Две дереференции: сначала offsets[i], затем data[start..end]. Cache-friendly при sequential scan, но random access = 2 cache lines.
Nested (List, Struct)Рекурсивная структура: parent буферы + child arrays. List = offsets + child. Struct = только children. Каждый child — полноценный Arrow array со своими буферами.
Dictionary EncodedДва массива: indices (int32/int8/int16) + dictionary values. Декодирование: value = dict_values[indices[i]]. Один dictionary шарится между RecordBatch в IPC.

Инспекция реального массива

В 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
NOTE

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 countarr.null_count
Validity bitmaparr.buffers()[0].hex()
Data buffer rawarr.buffers()[1].hex()
RecordBatch buffersbatch.column(i).buffers()
Dictionary valuesarr.dictionary
Dictionary indicesarr.indices
Общий размерpa.total_allocated_bytes()

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

  1. Каждый тип = фиксированный набор буферов. Int32 — 2 буфера (validity + data). Utf8 — 3 (validity + offsets + data). Struct — только validity + children. Viewer показывает это наглядно.

  2. Null-слоты занимают место. В data buffer null-элемент имеет слот с undefined bytes. Это сохраняет O(1) random access: data[i × sizeof] всегда валидный offset, значение проверяется по bitmap.

  3. Offsets = N+1 элементов. Для N строк нужен N+1 offset — последний указывает конец данных. Это позволяет вычислить длину любого элемента за O(1).

  4. Children — полноценные arrays. В List и Struct child arrays имеют собственные буферы, bitmap, и тип. Структура рекурсивна: List<Struct<name:Utf8, scores:List<Float64>>> = 4 уровня вложенности.

  5. Dictionary = indices + values. Выигрыш при low cardinality: 1M строк × 10 уникальных → 4MB indices + ~100B dict вместо ~10MB строк.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Arrow Utf8 массив из 1000 строк: offsets buffer содержит 1001 int32 значение. Последний offset = 48000. Как вычислить длину строки с индексом 500?

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

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

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

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