1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8

# теория · шаг 4 из 8

Коды целых чисел: разности, гамма Элиаса, Райс и varint

Коротко

Поисковый индекс хранит для каждого слова отсортированный список номеров документов, где оно встречается. По 32 бита на номер - расточительно: соседние номера близки, и выгоднее хранить разности между ними - маленькие числа. Маленькие числа кодируют кодами переменной длины, которым не нужна таблица частот. Гамма Элиаса: длина двоичной записи в унарном виде, потом сама запись - 2·log₂ x + 1 бит. Голомб-Райс с параметром k: частное x / 2^k унарно, остаток - k битами; при удачном k близок к пределу для разностей случайных номеров. varint: по 7 бит в байт и бит «есть продолжение» - грубее по сжатию, зато быстро и выровнено по байтам. На 20 тысячах номеров: 32 бита → 14 (varint), 16 (гамма), 10.5 (Райс).

Если ты знаешь, зачем хранить разности, как устроены гамма Элиаса, код Райса и varint и какой когда брать, листай до «Теперь сам».

Как думаешь, сколько бит нужно на номер документа

Слово встречается в 20 тысячах документов из десяти миллионов. Список номеров: 437, 1012, 1203, 2899, …. По 32 бита на номер - 640 тысяч бит.

Прежде чем читать дальше, ответь себе: номера отсортированы, и в среднем соседние отличаются на 500. Сколько бит нужно, чтобы записать число около 500? И как декодеру понять, где кончается одно число и начинается следующее, если у них разная длина?

На что это похоже

Журнал пробега машины. Можно записывать каждый раз показание одометра целиком: 128 437, 128 512, 128 601. Можно записывать, сколько проехал с прошлой записи: 75, 89 - короче в разы, а одометр восстанавливается сложением. Остаётся договориться, как писать числа разной длины подряд без разделителей, - и каждый код целых чисел это своя договорённость.

Механизм: разности и три кода

РАЗНОСТИ: d_1 = a_1 + 1,  d_i = a_i − a_(i−1)  (все ≥ 1 при различных a)
восстановление: префиксные суммы

ГАММА ЭЛИАСА, x ≥ 1:  (длина двоичной записи − 1) нулей + двоичная запись
    1 → 1,  2 → 010,  5 → 00101,  500 → 00000000111110100
    длина 2·⌊log₂ x⌋ + 1; декодер считает нули и читает на 1 бит больше

ГОЛОМБ-РАЙС с параметром k:  q = (x − 1) // 2^k,  r = (x − 1) % 2^k
    q единиц, ноль, затем r ровно k битами
    k = 8: 500 → q = 1, r = 243 → 10 11110011  (10 бит)
    хорош, когда разности примерно одного порядка: 2^k ≈ 0.69 · среднее

VARINT: число по 7 бит, в каждом байте старший бит = «дальше есть байт»
    500 = 3·128 + 116 → байты [116 + 128, 3] - 2 байта, 16 бит
    кратно байтам - декодер не возится с битами

Откуда 0.69: если номера разбросаны случайно, малые разности встречаются чаще больших, и вероятность падает с ростом разности по показательному закону; для таких данных лучший параметр кода Голомба - около ln 2 · среднее. Предел сжатия для таких разностей - примерно log₂(e · среднее) бит, для среднего 500 это 10.4; код Райса приходит почти к нему.

Правило

Отсортированные номера хранят разностями: числа маленькие, порядок восстанавливается сложением. Разности кодируют кодом переменной длины без таблицы: гамма Элиаса - 2·log₂ x + 1 бит, без параметров; Голомб-Райс - частное унарно и k бит остатка, с 2^k ≈ 0.69 · среднее почти достигает предела; varint - по 7 бит в байт с флагом продолжения, хуже по размеру, но быстрее и выровнен по байтам.

Разбор: пять номеров тремя способами

номера: 3 7 8 20 21     разности: 4 4 1 12 1

гамма:  4 → 00100   4 → 00100   1 → 1   12 → 0001100   1 → 1
        00100 00100 1 0001100 1           = 19 бит
Райс k = 2:  x − 1 = 3, 3, 0, 11, 0
        3 → 0 11,  3 → 0 11,  0 → 0 00,  11 = 2·4 + 3 → 110 11,  0 → 0 00
        011 011 000 11011 000             = 17 бит
varint: каждая разность меньше 128 → по байту, 5 байт = 40 бит
по 32 бита: 160 бит

Декодирование гаммы: 00100 - два нуля, значит дальше три бита 100 = 4. Код Райса: единицы до нуля - частное, затем ровно k бит - остаток. Ни одному коду не нужны разделители.

Разбор: список из двадцати тысяч номеров

Программа берёт 20 тысяч случайных номеров из десяти миллионов, кодирует разности тремя способами, раскодирует и сверяет, затем сравнивает размеры.

celye.py
import random

rng = random.Random(32)
dokumenty = sorted(rng.sample(range(10 ** 7), 20000))   # где есть слово
raznosti = [dokumenty[0] + 1] + [b - a for a, b in
                                 zip(dokumenty, dokumenty[1:])]


def gamma(x):                         # Элиас: нули + двоичная запись
    b = format(x, "b")                # x ≥ 1
    return "0" * (len(b) - 1) + b


def iz_gamma(bity, i):
    nuli = 0
    while bity[i + nuli] == "0":
        nuli += 1
    return int(bity[i + nuli:i + 2 * nuli + 1], 2), i + 2 * nuli + 1


def rajs(x, k):                       # Голомб-Райс: частное x // 2^k
    q, r = divmod(x - 1, 2 ** k)      # единицами, остаток - k битами
    return "1" * q + "0" + format(r, f"0{k}b")


def iz_rajsa(bity, i, k):
    q = 0
    while bity[i + q] == "1":
        q += 1
    r = int(bity[i + q + 1:i + q + 1 + k], 2)
    return q * 2 ** k + r + 1, i + q + 1 + k


def varint(x):                        # по 7 бит в байт, старший бит -
    out = []                          # «дальше ещё байт»
    while True:
        x, b = divmod(x, 128)
        out.append(b + (128 if x else 0))
        if not x:
            return out


def raspakovat(potok, chitat):
    i, chisla = 0, []
    while i < len(potok):
        x, i = chitat(potok, i)
        chisla.append(x)
    return chisla


srednee = (dokumenty[-1] + 1) / len(dokumenty)
k = len(format(round(srednee * 0.69), "b")) - 1   # 2^k ≈ 0.69·среднее
g = "".join(map(gamma, raznosti))
r = "".join(rajs(x, k) for x in raznosti)
v = [b for x in raznosti for b in varint(x)]
assert raspakovat(g, iz_gamma) == raznosti
assert raspakovat(r, lambda p, i: iz_rajsa(p, i, k)) == raznosti
print(f"номеров {len(dokumenty)}, средняя разность {srednee:.0f},"
      f" k = {k}")
for imya, bit in (("по 32 бита", 32 * len(dokumenty)),
                  ("разности + varint", 8 * len(v)),
                  ("разности + гамма", len(g)),
                  ("разности + Райс", len(r))):
    print(f"{imya:18} {bit:7} бит,"
          f" {bit / len(dokumenty):5.1f} на номер")
↳  выводpython3 celye.py
номеров 20000, средняя разность 500, k = 8
по 32 бита          640000 бит,  32.0 на номер
разности + varint   284184 бит,  14.2 на номер
разности + гамма    326156 бит,  16.3 на номер
разности + Райс     209875 бит,  10.5 на номер

Разности ужали список втрое уже простым varint. Гамма здесь хуже varint: она не знает, что числа около 500, и тратит на длину столько же бит, сколько на само число. Код Райса с подобранным k дал 10.5 бита на номер - в трёх десятых бита от предела 10.4.

Что ломается без этого

Первая ошибка - кодировать сами номера, а не разности: номера огромные, и никакой код переменной длины не поможет. Вторая - подобрать k у Райса не по данным: при k слишком малом частное становится длинным унарным хвостом, при слишком большом остаток тратит лишние биты на каждом числе. Третья - гамма для нуля: она начинается с единицы, и ноль надо сдвигать (x + 1); у разностей различных номеров нулей нет, а у разностей с повторами - есть. Четвёртая - читать varint, забыв про флаг продолжения на последнем байте: декодер «съест» следующий номер.

Зачем это в работе

Инвертированные индексы поисковиков и баз данных (Lucene, Elasticsearch) хранят списки документов разностями с битовой упаковкой и кодами вроде Райса. varint - формат целых в Protocol Buffers, в сетевых протоколах и в файлах SQLite. Коды Голомба сжимают без потерь звук (FLAC) и изображения (JPEG-LS), где остатки предсказания малы. Разности перед сжатием - стандартный ход для отметок времени, координат треков, показаний датчиков: соседние значения близки, и сжимать надо изменение, а не значение.

Теперь сам

Закодируй число 9 гаммой Элиаса и кодом Райса с k = 2. Какой код короче?

Ответ. Двоичная запись 9 - 1001, четыре знака: гамма - три нуля и 1001: 0001001, 7 бит. Райс: x − 1 = 8, q = 8 // 4 = 2, r = 0: 11 0 00 - 11000, 5 бит. Короче Райс: при k = 2 числа около 4..12 - его родной диапазон.

Главное

Отсортированные номера хранят разностями, а разности - кодами переменной длины без таблицы частот: гамма Элиаса (длина унарно, потом число), Голомб-Райс (частное унарно, k бит остатка, 2^k ≈ 0.69 · среднее), varint (по 7 бит в байт). На 20 тысячах номеров: 32 бита на номер у сырого списка, 14 у varint, 10.5 у Райса - у самого предела для случайных разностей.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

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