Learning Platform
Глоссарий Troubleshooting
Урок 10.05 · 35 мин
Продвинутый
ALPFloating-Point CompressionIEEE 754Decimal FloatsDuckDBFastLanes FORG-ALPCWI Amsterdam

ALP: адаптивное сжатие float

В Модуле 08, Урок 03 мы видели, как FOR, Delta и BitPacking сжимают целые числа до 2–4 бит на значение. Но все эти кодировки работают с integers. Что делать с float64? В Модуле 08, Урок 05 мы кратко упомянули BYTE_STREAM_SPLIT (Parquet 2.8) — но он даёт лишь ~1.5–2x на типичных float-данных.

ALP (SIGMOD 2024, Afroozeh, Kuffó & Boncz — CWI Amsterdam) решает проблему радикально: превращает float в integers и сжимает их FastLanes FOR-кодировкой. Звучит как трюк, но за ним стоит наблюдение о реальных данных.

Почему IEEE 754 float так плохо сжимается

IEEE 754 double: анатомия 64-bit float

Число: 10.12 (double precision, 8 байт)

Пример: число 10.12 в IEEE 754 double precision. Человек видит '10.12' — компьютер хранит 64 бита: sign (1 bit) + exponent (11 bits) + mantissa (52 bits).
IEEE 754 binary64 representation
Sign: 01 бит. 0 = положительное число. Единственный предсказуемый бит — часто 0 для числовых данных.
Exponent: 1000000001011 бит. Biased exponent: 1026 - 1023 (bias) = 3. Значит: 1.mantissa × 2³. Экспонента варьируется между значениями — Delta encoding на ней неэффективен.
Mantissa: 0100001111010111...52 бита. Для числа 10.12: мантисса = 0100001111010111000010100011110101110000101000111101... Выглядит как случайные биты — потому что 10.12 не имеет точного двоичного представления. 10.12 = 10.11999999999999... в binary. Хвост мантиссы — артефакт конвертации base-10 → base-2.

52 бита мантиссы ≈ random noise → FOR, Delta, Dict бесполезны

Проблема: мантисса занимает 52 из 64 бит и выглядит как noise. FOR/Delta бесполезны — нет паттерна в битах. Dictionary — бесполезен при high cardinality (уникальные значения). RLE — бесполезен (нет повторов). Единственный вариант до ALP: generic byte-level compression (Zstd) или BYTE_STREAM_SPLIT.

Проблема — не в float как абстракции, а в IEEE 754 encoding. Число 10.12 — простое для человека, но в binary его мантисса — 52 «случайных» бита, потому что 0.12 не имеет точного двоичного представления.

Наблюдение ALP: большинство float — это decimals

ALP начинается с простого наблюдения: в реальных данных большинство float-значений — это decimal числа, записанные в float по привычке:

Реальные float-данные: decimals в маскировке
ДоменОбласть данных
ПримерТипичные значения
ТочностьФактическая десятичная точность данных
ФинансыЦены, суммы транзакций, курсы валют. Всегда decimal: $3.25, €10.99, ¥1234.56. Хранятся как float64 потому что Pandas/Spark по умолчанию читают числа как double.
$3.25, €10.992 десятичных знака — стандарт для валют.
2 знака (×100)Ровно 2 десятичных знака. Умножение на 100 превращает $3.25 → 325 (integer).
СенсорыТемпература, давление, влажность. Датчики имеют ограниченную точность: 0.1°C, 0.01 атм. Данные хранятся как double, но реально содержат 1–3 десятичных знака.
36.6, 101.325Температура тела, давление. 1–3 десятичных знака.
1–3 знака (×10–1000)1–3 десятичных знака. ×10: 36.6→366. ×1000: 101.325→101325.
ГеоданныеШирота/долгота: 55.753215, 37.621356. GPS точность — 6 знаков (~0.1 метра). Хранятся как double.
55.753215, 37.6213566 десятичных знаков. ×10⁶: 55.753215 → 55753215 (integer, 26 бит).
6 знаков (×10⁶)6 десятичных знаков. Умножение на 10⁶ даёт integer ≤ 180000000 — влезает в 28 бит.
НаучныеРезультаты вычислений, симуляций. Полная double precision — 15–17 значащих цифр. НЕ decimal — реально используются все 52 бита мантиссы.
3.14159265358979315+ значащих цифр. Это НЕ decimal — ALP Mode A не подходит.
15+ знаков → Mode BСлишком много знаков для умножения (переполнение int64). ALP использует Mode B — другой алгоритм.

Вывод: финансовые, сенсорные и геоданные — это decimal числа, хранимые в float64 «по инерции». Если умножить их на 10^n и округлить — получим integers, которые отлично сжимаются FOR/BitPacking.

ALP Mode A: decimal float → integer → FastLanes FOR

Mode A — основной алгоритм ALP. Применяется, когда float-значения в блоке можно losslessly конвертировать в integers:

ALP Mode A: pipeline decimal float → integer → compressed

Блок: 1024 × float64 цен (3.25,3.25, 10.99, $7.50, …)

Блок float64: 1024 значения цен. $3.25, $10.99, $7.50, $15.00, ... Все — 2 десятичных знака. 8 байт × 1024 = 8 KB несжатых данных.
Шаг 1: определить множитель (factor)
Factor detection: n = 2 (×10² = ×100)ALP сканирует sample значений из блока. Для каждого пробует: ×10, ×100, ×1000, ... Выбирает минимальный n, при котором ВСЕ значения в sample после умножения на 10^n дают exact integer (round-trip: int → float → ×10^(-n) = original). Для цен: n=2 — $3.25 × 100 = 325 (integer). Проверка: 325 / 100.0 = 3.25 + (exact).
Шаг 2: умножить все значения на 10^n
Float → Integer: ×100Каждое значение × 100: $3.25→325, $10.99→1099, $7.50→750, $15.00→1500. Результат: массив int64. Проверка round-trip для каждого значения: если val × 100 → round → ÷ 100 ≠ val — значение помечается как exception.
Шаг 3: exceptions — значения, не конвертируемые losslessly
Exceptions: 0 из 1024 (все lossless)Если все значения в блоке — ровно 2 десятичных знака, exceptions = 0. Если бы было значение $3.256 (3 знака) — оно не конвертируется losslessly при n=2 (3.256 × 100 = 325.6 ≠ integer). Два варианта: (1) увеличить n до 3, (2) пометить как exception. ALP выбирает n, минимизирующий exceptions.
Шаг 4: FastLanes FOR на integers
FOR: base=325, offsets [0, 774, 425, 1175, ...]Frame-of-Reference: base = 325 (min). Offsets: 1099-325=774, 750-325=425, 1500-325=1175. Max offset: 1175 → bit_width = 11 бит. 1024 × 11 бит = 1408 байт. UTL-порядок для SIMD-friendly unpacking.

~1.4 KB вместо 8 KB. Ratio: ~5.7x. Decode: integer ops only.

Финальный размер: 1408 (offsets) + 8 (base) + 1 (factor) + 0 (exceptions) = ~1.4 KB. Оригинал: 8 KB. Ratio: 8/1.4 ≈ 5.7x. Для сравнения: Zstd на raw float64 даёт ~2x. ALP: 5.7x — и decode быстрее (integer arithmetic, no entropy decoding).

Round-trip гарантия

ALP lossless — ни один бит информации не теряется. Гарантия обеспечивается round-trip проверкой для каждого значения:

encode(val, n):
 int_val = round(val × 10^n)
 check = int_val × 10^(-n)
 if check == val: → store int_val (compressed)
 else: → store as exception (raw float64)

Если хотя бы одно значение не проходит round-trip — оно хранится в exception list (позиция + raw float64). В типичных данных (финансы, сенсоры) exceptions составляют 0–2% блока.

NOTE

Pseudodecimal encoding в BtrBlocks (Урок 03) — предшественник ALP Mode A. Обе идеи: «float, который на самом деле decimal, конвертируем в integer». Отличие: ALP формализует round-trip проверку, интегрирует FastLanes UTL layout для SIMD decode, и добавляет Mode B для non-decimal floats. BtrBlocks Pseudodecimal работает только с Mode A-подобными данными.

ALP Mode B: left-part dictionary для high-precision

Mode A не работает для научных данных (π = 3.141592653589793…) — умножение на 10^15 переполняет int64. Для таких данных ALP использует Mode B: разбиение float на две части.

ALP Mode B: left-part dictionary encoding

Блок: 1024 × float64 high-precision (π, e, …)

Блок float64 с высокой точностью: 3.141592653589793, 3.141592653589794, 2.718281828459045, ... Full 52-bit mantissa. Mode A невозможен — слишком много значащих цифр.
Разбиение на left-part и right-part
Left-part (старшие биты)Старшие 32 бита float64 (sign + exponent + верхние 20 бит мантиссы). Содержат 'значащую часть' числа. Для колонки с данными одного типа (все температуры, все координаты) left-parts часто повторяются — low cardinality.
Right-part (младшие биты)Младшие 32 бита мантиссы. Содержат 'хвост точности'. Более случайные, но в данных одного источника (один датчик, одна формула) — могут иметь паттерны.
Encoding
Left: Dictionary → FastLanesLeft-parts кодируются Dictionary encoding: если 1024 float64 из колонки температур — left-parts могут иметь 50–200 уникальных значений. Dictionary → indices → FastLanes FOR/BitPack на indices.
Right: FastLanes BitPackRight-parts (32-bit integers) кодируются напрямую FastLanes BitPack. Если precision реально ниже 32 бит — bit_width будет меньше 32, и BitPack сожмёт.

Decode: left_dict[idx] | right → original float64. Ratio: 1.5–3x

Decode: load left-part dict + indices, load right-parts. Reconstruct: concat(left[dict[idx]], right). SIMD-friendly: обе части используют FastLanes UTL layout. Ratio: 1.5–3x на scientific data (зависит от реальной энтропии). Хуже чем Mode A на decimal, но лучше чем Zstd на structured float data.

Автоматический выбор режима

ALP автоматически определяет, какой режим использовать для каждого блока:

ALP: автоматический выбор Mode A vs Mode B

Блок: 1024 × float64 (тип неизвестен)

Блок float64: 1024 значения. ALP не знает заранее, decimal это или high-precision.
Sample 64 значений → пробуем Mode A
Mode A probe: перебор n ∈ [0..18]Для каждого n от 0 до 18: пробуем sample × 10^n → round → ÷ 10^n. Считаем: сколько значений прошли round-trip (lossless). Если ≥ 90% sample → Mode A с этим n. Если ни один n не даёт 90% → Mode B.
n=2 даёт 98% → Mode A
98% → Mode A (n=2)98% значений конвертируются losslessly при n=2. Оставшиеся 2% → exception list. Mode A выбран: основной путь — integer compression.
Если бы {'<'} 90% → Mode BЕсли ни один n не даёт ≥90% lossless — данные не decimal. Переключаемся на Mode B: left-part dictionary.
TIP

Порог 90% — это эвристика. ALP оценивает: если Mode A покрывает ≥90% блока, exception overhead (raw float64 per exception) компенсируется сжатием основной массы. Если данные смешанные (часть decimal, часть high-precision) — ALP может выбрать Mode A с exceptions или Mode B без exceptions, в зависимости от того, что даёт меньший размер.

G-ALP: GPU-расширение (DaMoN 2025)

G-ALP (Hepkema, Afroozeh, Felius, Boncz, Manegold — DaMoN 2025) переносит ALP на GPU. Ключевая адаптация: GPU имеют тысячи threads, но каждый thread — слабее CPU-ядра. G-ALP использует ту же двухрежимную стратегию (Mode A / Mode B), но с layout, оптимизированным под GPU warp (32 threads) вместо CPU SIMD lanes.

ALP vs G-ALP: CPU lanes vs GPU warps
ПараметрХарактеристика
ALP (CPU)Оригинальный ALP для CPU
G-ALP (GPU)GPU-расширение G-ALP
Единица параллелизмаМинимальная единица параллельного выполнения
SIMD lane (8–16)CPU SIMD: 8 lanes (AVX2) или 16 lanes (AVX-512). FastLanes UTL оптимизирован под эти размеры.
GPU warp (32 threads)GPU warp: 32 потока исполняются синхронно (SIMT). G-ALP адаптирует UTL layout под warp size.
Размер блокаКоличество значений в одном блоке кодирования
1024 значенийFastLanes: 1024 — оптимально для L1/L2 cache (8 KB для float64).
1024 на warpG-ALP: тот же размер блока, но обрабатывается одним warp (32 threads × 32 значения/thread).
Decode throughputСкорость декодирования
Десятки GB/sALP CPU: десятки GB/s на одном ядре.
Сотни GB/sG-ALP GPU: сотни GB/s на A100/H100 — GPU memory bandwidth-bound.

Итог

ALP превращает «несжимаемые» float64 в хорошо сжимаемые integers — без потери ни одного бита. Ключевые идеи:

  • Mode A (decimal floats): умножение на 10^n → integer → FastLanes FOR. Ratio: 4–8x на финансах/сенсорах
  • Mode B (high-precision): split на left/right parts → dictionary + bitpack. Ratio: 1.5–3x
  • Автовыбор: sample-based probing, порог ~90% lossless для Mode A
  • Round-trip гарантия: каждое значение проверяется, non-lossless → exception list
  • Интегрировано в DuckDB как internal float compression
  • G-ALP (DaMoN 2025): GPU-версия для A100/H100

В следующем уроке — FSST: как CWI решила аналогичную проблему для строковых данных, где dictionary encoding не справляется.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. ALP Mode A: float 19.99 × 10² = 1999 (integer). Какое условие должно выполняться для lossless-конверсии?

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

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

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

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