Сжатие целых чисел
Цель статьи осветить state of the art методы сжатия целых чисел, чтобы сэкономить в будущем время исследования алгоритмов и терминологии. При этом описание части алгоритмов может быть упрощено для понимания. Сравнение алгоритмов тоже находится вне рамках этой статьи. Подробнее можно почитать в ссылках.
Многие из упомянутых ниже алгоритмов используются в прикладных задачах: сжатие битмап, обратных индексов, просто массивов данных.
Введение
Различные языки программирования предлагают типы целых чисел.
Типы данных:
8 бит |
byte |
int8 |
uint8 |
---|---|---|---|
16 бит |
short |
int16 |
uint16 |
32 бита |
integer |
int32 |
uint32 |
64 бита |
long |
int64 |
uint64 |
128 бит |
long long |
Количество бит и поддержка отрицательных чисел определяют диапазон поддерживаемых значений.
Диапазоны значений:
Биты |
signed |
unsigned |
---|---|---|
8 |
-128 ... 0 … 127 |
0 … 255 |
16 |
-32768 … 0 … 32767 |
0 … 65535 |
32 |
-2147483648 … 2147483647 |
0 … 4294967295 |
64 |
-9223372036854775808 … 9223372036854775807 |
0 … 18446744073709551615 |
Современные компьютеры имеют 64х разрядные шины данных, соответственно за один цикл можно передавать 64 бита.
Самым практичным типом является int32, потому что в него помещаются большинство значений, которыми оперируют программы. При этом часто 32 бита гораздо больше, чем необходимо для хранения информации.
Рассмотрим пример массива:
[1, 2, 3, 4, 5, 65536, 10]
Видно, что большинство чисел занимают тип int8, но из-за шестого элемента - 65536, тип массива должен быть int32.
Какие алгоритмы сжимают данные?
Run-length encoding
Если в массиве часто встречаются повторяющиеся значения, можно запомнить их количество. В магазинных чеках пишут одну строку - 10 яблок, а не десять строк “яблоко”.
Пример с оценками студентов по предметам:
Студент |
Урок 1 |
Урок 2 |
---|---|---|
1 |
5 |
4 |
2 |
5 |
4 |
3 |
5 |
3 |
4 |
5 |
5 |
5 |
5 |
5 |
6 |
5 |
5 |
7 |
5 |
5 |
Массив |
[5,5,5,5,5,5,5] |
[4,4,3,5,5,5,5] |
Размер массива |
28 байт |
28 байт |
RLE |
(5, 7) |
[(4, 2), (3, 1), (5, 4)] |
Размер после RLE |
8 байт |
24 байта |
Delta encoding
Бывает такое, что числа большие, но близкие, такие как 1000001 и 1000002. Разница между ними равна 1. Значит, можно сохранить первое число в массиве без изменений, а потом записать разницу от предыдущего числа. Для первого элемента используется тип, который необходим. Для остальных элементов берется тип поменьше (int8,…).
Пример:
Массив до |
Размер до |
Массив после |
Размер после |
---|---|---|---|
[10000000001, 10000000002, 10000000003, 10000000004, 10000000007, 10000000008, 10000000010, 10000000011] |
int64[8], получаем 64 байта |
10000000001, [1, 2, 3, 6, 7, 9, 10] |
int64, int8[7], получаем 15 байт |
Так же можно использовать Delta of delta encoding. Такой подход используется для кодирования времени: временные метки в системах мониторинга часто отличаются на какое-то фиксированное число с небольшими отклонениями, потому что период сбора данных примерно одинаковый.
Идея битового сжатия
Когда 32 бита много, использовать меньше.
Примеры:
Число |
Биты int32 |
Удалили предшествующие нули |
---|---|---|
1 |
00000000000000000000000000000001 |
1 |
2 |
00000000000000000000000000000010 |
10 |
3 |
00000000000000000000000000000011 |
11 |
4 |
00000000000000000000000000000100 |
100 |
5 |
00000000000000000000000000000101 |
101 |
65536 |
00000000000000010000000000000000 |
10000000000000000 |
10 |
00000000000000000000000000001010 |
1010 |
28 bytes |
28 bytes |
4 bytes |
Просто удалив предшествующие нули, ничего нельзя прочитать, потому что неизвестно сколько нулей убрали. Но на основе этой идеи есть множество алгоритмов, которые стараются использовать только необходимое количество бит для хранения целых чисел. При этом дополнительно резервируется место для вспомогательной информации, чтобы суметь раскодировать значения.
Variable Byte (Varint, Group Varing Encoding, …)
История берет свое начало с 1990 года и Variable byte (varint) алгоритма. Базовый алгоритм использует 7 бит для значимой информации. Восьмой (старший) бит используется, как индикатор, является ли следующий байт продолжением предыдущего.
Примеры:
Число |
Битовое представление (int32) |
VB encoded |
Размер после |
---|---|---|---|
16385 |
00000000000000000100000000000001 |
10000001 10000000 00000001 |
3 байта |
1 |
00000000000000000000000000000001 |
00000001 |
1 байт |
1956 |
00000000000000000000011110100100 |
10100100 00001111 |
2 байта |
Подробнее рассмотрим первый пример. Последние 7 бит: 0000001 (младшие биты справа) становятся первыми битами в закодированном значении. Затем следующие 7 бит: 0000000, - вторым байтом. Оба байта в закодированном виде начинаются с единичного старшего бита, поэтому следующий байт продолжение предыдущего. Наконец старший значащий бит исходного числа находится на пятнадцатой позиции. Он запишется в младший бит третьего байта закодированного значения. У этого байта старший бит будет 0, значит, число закончилось.
Этот алгоритм широко известен. В частности, используется в формате Protobuf, который применяется в gRPC.
PforDelta (NewPforDelta, OptPforDelta)
Алгоритм состоит из нескольких шагов:
Посчитать все дельты.
Для каждого блока из 128 дельт найти такое количество бит B, которым можно закодировать до 90% всех чисел.
Перекодировать все числа, используя B.
Числа, которые нельзя закодировать B битами, записываются, как исключения в конце.
Пример:
Массив до |
Массив дельт |
Бинарный вид после алгоритма: 4 бита на дельту и одно 8 байтное исключение в конце. |
---|---|---|
[10000000001, 10000000002, 10000000003, 10000000004, 10000000007, 10000000008, 10000000010, 10000000011] |
[10000000001, 1, 2, 3, 6, 7, 9, 10] |
100000000001001000110110011110011010, 0000000000000000000000000000001001010100000010111110010000000001 |
Для краткости рассмотрим блок из 8 дельт. После преобразования достаточно 4х бит (десять - самое большое, это 1010) для хранения всех чисел, кроме первого. Первые 4 бита использовано, чтобы закодировать одно исключение на позиции 0. Дальше 4 бита пустые - как раз те, что на позиции ноль. Следующие четыре бита: 0001 - это единица из второй позиции исходного массива, затем двойка 0010 и т.д. Это иллюстрация, а не точная имплементация. Итого получим 13 байт вместо 64 байт.
Расширения алгоритма оптимизируют хранение исключений и поиск B.
Simple9b (Simple16b, Simple8b)
Этот алгоритм записывает как можно большее количество чисел в 32-битном слове. 4 бита используются для хранения состояния, 28 бит для данных.
Состояние - выбранный способ хранения данных в 28 битах, который зависит от того, сколько бит требуется для каждого числа.
Заранее составляются 9 комбинаций состояний: 281 бит (0001), 142 бита (0010), 93 бита (0011), 74 бита (0100), 5*5 бит (0101), и т.д. Комбинации фиксированы для алгоритма.
Пример:
Массив |
Слово после Simple9b |
---|---|
[5,5,5,5,5,5,5] |
01000101010101010101010101010101 |
Массив состоит из 7 значений по 3 бита, т.к. 5 - это 101. Используя допустимые комбинации, сохраняем 7 значений по 4 бита с маской 0100 в начале. Итого 4 байта против 28 байт.
В 4 битах можно закодировать 16 комбинаций, этим пользуется Simple16b. Simple8b использует 64х разрядное слово.
Roaring
Комбинированный алгоритм сжатия разделяет пространство положительных целых чисел на диапазоны [0, 65535], [65536, 65536 × 2 − 1], [65536 × 2, 65536 × 3 − 1] и т.д. По построению целые числа в каждом диапазоне имеют одинаковые два старших байта. В зависимости от того сколько чисел N попадает в диапазон выбирается один из двух вариантов хранения:
N > 4096, используется 65536 разрядная битмапа.
Иначе, сортированный массив int16.
Особенностью реализации roaring-алгоритма является то, что она поддерживает операции пересечения и объединения, которые можно использовать в поиске.
Выводы
Сжатие данных обозначает декомпрессию на чтениях - это потеря времени. В настоящие дни имплементации алгоритмов стараются утилизировать векторные операции процессора для ускорения. Бенчмарки методов сжатия обычно сравниваются по гигабайтам в секунду, которые обрабатывает имплементация.
Все это говорит о том, что эти алгоритмы заточены под пакетную обработку данных. Но, что если после сжатия массива целых чисел, все еще нужен доступ по индексу? Об этом в следующей статье.
Ссылки
Комментарии (19)
shasoftX
13.01.2024 12:06+6Сжатие данных обозначает декомпрессию на чтениях - это потеря времени. В настоящие дни имплементации алгоритмов стараются утилизировать векторные операции процессора для ускорения. Бенчмарки методов сжатия обычно сравниваются по гигабайтам в секунду, которые обрабатывает имплементация.
Это на эльфийском?
В статье не столько алгоритмы сжатия, сколько алгоритмы трансформации данных.
Все это говорит о том, что эти алгоритмы заточены под
пакетнуюпотоковую обработку данных. Но, что если после сжатия массива целых чисел, все еще нужен доступ по индексу? Об этом в следующей статье.Скорее всего речь тут о том, что для получения какого-то значения из упакованного потока нужно распаковать весь поток перед этим. Но написано тоже на эльфийском диалекте
venicum Автор
13.01.2024 12:06Скорее всего речь тут о том, что для получения какого-то значения из
упакованного потока нужно распаковать весь поток перед этим.Не всегда есть необходимость распаковывать весь поток, можно указать смещения и распаковать часть данных.
О потоках речь не идет, потому что поток - это байты, которые поступают. Здесь это не важно, эти же алгоритмы применяются для архивации иммутабельных данных.shasoftX
13.01.2024 12:06+2Это значит что данные пакуют блоками. Как раз чтобы не распаковывать с самого начала из-за пары байт в конце.
Сам так делал когда писал читалку для мобильного. Чтобы можно было произвольно перемещаться по тексту разбивал весь текст на блоки по 4096 байт и каждый блок упаковывал отдельно + в начале индекс на начало каждого блока.
MaxAkaAltmer
13.01.2024 12:06+4Статья похожа на беглый обзор методов, ну ок, а почему ни слова о префиксных кодах? Это целый класс алгоритмов именно для компактного представления целых чисел.
venicum Автор
13.01.2024 12:06Спасибо, что упомянули. Не наткнулся на них, когда искал обзоры. Подскажете материал?
in_heb
13.01.2024 12:06+2При каких условиях long long имеет 128 бит (язык, компилятор, cpu)? Ну и в целом ощущение что статья написана chat gpt (но зачем?)
venicum Автор
13.01.2024 12:06+1При каких условиях long long имеет 128 бит (язык, компилятор, cpu)?
Спасибо, что заметили. Такие компиляторы похоже необщеприняты.
При этом long long в плюсах может быть >= 64 бит: https://stackoverflow.com/questions/2143157/is-the-type-long-long-always-64-bits?rq=3.
Пожалуй, лучше было упомянуть long double, тут есть известные 128битные реализации: https://www.ibm.com/docs/ru/aix/7.1?topic=sepl-128-bit-long-double-floating-point-data-type
adeshere
13.01.2024 12:06При каких условиях long long имеет 128 бит (язык, компилятор, cpu)?
В фортране вместо всяких "long long" обычно пишут числом (1, 2, 4, 8, 16...). Это полезно
для переносимости программ
число байт - оно ведь на всех архитектурах число, причем можно быть уверенным, что почти всегда то же самое ;-)
На фоне современных языков такой стиль может показаться архаичным, зато можно быть почти уверенным, что если программа компилируется под какую-то архитектуру без грязных ругательств, то скорее всего, она будет под ней работать так, как задумано. То есть 4-байтовый real/integer не превратится прозрачно для программиста в 8-байтовый, и наоборот ;-)
Однако, насколько я знаю, поддержка типа integer(kind=16) все еще зависит от компилятора. В частности, GNU фортран вроде бы поддерживает, начиная с G95. А вот мой Intel фортран 2013г - не поддерживает. Хотя real(kind=16) у меня есть и вполне себе работает.
Что интересно,
несмотря на заявления о полной совместимости компилятора со Студией 2008 (в которую Intel Fortran XE 13 действительно интегрируется), встроенный в VS2008 отладчик на моих real(16) конкретно глючит. Я сперва не мог понять, что происходит: открываю в отладчике переменную, а там бред какой-то. Но если не смотреть под отладчиком, а просто напечатать значение или присвоить его куда-то, то все Ок. Т.е. глюк именно в VS..
in_heb
13.01.2024 12:06+2В фортране вместо всяких "long long" обычно пишут числом (1, 2, 4, 8, 16...). Это полезно
Ну да, в C для этого придумали всякие uint32_t (https://en.cppreference.com/w/c/types/integer ), конечно стало удобнее чем до этого когда каждый разработчик кросс-платформенных приложений изобретал свои костыли чтобы получить целое числа фиксированного количества бит
Almighty_Goose
13.01.2024 12:06+1Сжатие данных обозначает декомпрессию на чтениях - это потеря времени.
Вселенная не заканчивается на процессорах общего назначения. В проектах FPGA и микроконтроллерах, где ОЗУ измеряется в мегабитах (а то и в килобитах) есть место рационального использования ресурсов.
Вариантный метод довольно интересен, необязательно именно в виде, представленным в статье. Например, одним сервисным битом переключать 7 или 31 бит на полезную нагрузку.
venicum Автор
13.01.2024 12:06-1Вселенная не заканчивается на процессорах общего назначения. В проектах
FPGA и микроконтроллерах, где ОЗУ измеряется в мегабитах (а то и в
килобитах) есть место рационального использования ресурсов.Имел ввиду, время процессора. Конечно, для этого бывают свои причины.
Вариантный метод довольно интересен, необязательно именно в виде,
представленным в статье. Например, одним сервисным битом переключать 7
или 31 бит на полезную нагрузку.Далек от микроконтроллеров, но рад, что навело на какие-то мысли!
Jury_78
Может я совсем старый, но это точно по русски?
TIEugene
ChatGPT - он такой.
Бред несетЗагадочный.venicum Автор
Уверен, что не на английском! :)
sci_nov
Кажется, это разговорный стиль иностранных R&D :)