Думаю все, кто так или иначе интересовался сжатием информации или каким-то другим способом кодирования данных - слышали о кодах переменной длины. Я не корифей в этой области, поэтому мои познания ограничены кодами Элиаса и Голомба (причём последнее я только читал на вики и скорее знаю чисто академически).

Пару лет, уже в практическом смысле, а не только в умственных разминках, я стал заниматься изысканиями в области LZSS вариаций сжатия. В частности основными признаками были: окно на предыдущие данные, смещение и длина повтора. Изначально я пользовался фиксированными кодами, но быстро понял, что даже 1 бит без полезной нагрузки загромождал результирующий файл, особенно когда речь шла о тысячах бит. Вот тогда я и стал изобретать что-то с переменной длиной и как всегда, до изобретения очередного велосипеда - узнал о кодах Элиаса. Я ими пользуюсь и сейчас, но при кодировании смещений в окне я стал пользоваться способом который подсмотрел у Эйнара Каускаса в его ZX0.

Не скажу что Эйнар тут первооткрыватель, но он был для меня вдохновителем. Суть в том, что для больших значений имеет смысл представлять число не одним кодом, а делить его на старшую и младшую части - MSB и LSB. Это позволяет для значений MSB пользоваться меньшим числом бит при использовании кодов Элиаса. И это хорошо работает на практических данных.

Но позже я заметил, что например Эйнар пользуется границей для перехода к MSB в 128 (а не в 256 как можно было бы подумать), как он сам пишет - это даёт лучшие результаты (я их проверял практически и это действительно так). И получается, что при кодировании гамма-кодом Элиаса, как мне казалось, мы тратим больше бит, чем могли бы.

Особенность гамма-кода в том, что он быстро растёт, еще и ступенями по 2 бита, что например для значения в 126 уже требует 13 бит для кодирования. И так как меня в первую очередь интересовал интервал 0-127 (мы же помним, что Эйнар и мои тесты выбрали это значение как основную границу), где данные чаще всего равномерно распределены (даже при росте MSB значение LSB продолжает по кругу меняться от 0 до 127), то я стал думать как именно можно изменить представление через те же принципы, что и для кодов Элиаса, но с меньшим расходами битов.

В итоге я придумал вот что. Коды Элиаса симметричны и разделены надвое одним битом. Таким образом код всегда имеет нечётную длину, но число значащих бит справа задаётся числом нулей до разделителя в центре. То есть число бит кодируется не числовым представлением, а просто набором битов, фактически используемый бит "ноль" работает, а бит "единица" - нет.

Я же предлагаю левую часть кодировать фиксированным числом битов, в моём случае тремя битами.

(в таблице пример кодов и разница в количестве битов, минус - это показатель эффективности моего кода)

Число

Гамма-код Элиаса

Моя реализация

Число бит

0

нет

000

+3

1

1

001 0

+3

2

0 1 0

001 1

+1

3

0 1 1

010 00

+2

4

00 1 00

010 01

0

5

00 1 01

010 10

0

6

00 1 10

010 11

0

7

00 1 11

011 000

+1

8

000 1 000

011 001

-1

9

000 1 001

011 010

-1

10

000 1 010

011 011

-1

11

000 1 011

011 100

-1

12

000 1 100

011 101

-1

13

000 1 101

011 110

-1

14

000 1 110

011 111

-1

15

000 1 111

100 0000

0

16

0000 1 0000

100 0001

-2

31

0000 1 1111

101 00000

-1

63

00000 1 11111

110 000000

-2

126

000000 1 111110

110 111111

-4

Как видно из таблицы выше - я не использую весь диапазон, а ограничиваюсь только значением до 126, что укладывается в диапазон 0-127, используемый в ZX0 и в моём варианте LZSS. Если сравнивать с кодами Элиаса, то мой вариант проигрывает для значений 0-7, причем только для значений 0, 1, 2, 3 и 7 мы видим увеличение значения в битах, остальные значения или равнозначны или всегда даёт меньшее значение.

В итоге мы получаем более равномерный рост размера кода только по одному биту и для значения 126, например, разница между моим кодом и кодом Элиаса составляет уже 4 бита. Например для кодирования части MSB эти коды не подходят, так как на представление малых значений будет очень большой перерасход битов. Для значения MSB 7 это окно размером в 7*128 = 896 байт. Практические тесты показали достоверность этих умозаключений.

Как видно по коду в таблице я в первой части использую три фиксированных бита, которые говорят о размере последующего блока, это позволяет однозначно разделять код, например, 001 0 от кода 010 00, что и требуется для кодов переменной длины.

В тестах на разных типах файлов применение такого кодирования даёт прирост степени сжатия от 2 до 12%. Ну и чтобы не быть голословным я привожу таблицу результата кодирования моей реализацией LZSS на части набора файлов, который я взял из статьи пользователя Introspec.

Размер

Сжато LZSS (Элиас)

Сжато LZSS (авторский)

Эффективность %

[ Calgary ]

obj1

21 504

11 700

10 836

8,385

paper1

53 161

24 079

21 646

10,105

progc

39 611

17 051

15 318

10,163

[ Сanterbury ]

sum

38 240

14 778

13 599

7,978

xargs.1

4 227

2 252

1 996

11,367

fields.c

11 150

3 950

3 511

11,113

cp.html

24 603

10 181

9 240

9,242

grammar.lsp

3 721

1 549

1 394

10,006

[ Graphics ]

diver_mercenary_3_(2013).bsc

11 136

3 963

3 800

4,113

craig_stevenson-rewind_to_side_a_(2015).ch$

13 831

4 877

4 587

5,946

diver_tbk_logo_(2001).img

13 824

3 588

3 501

2,424

agyagos_graphics-robin_(2000).mc

12 288

3 735

3 533

5,408

bfox-dont_go_away_(2010).mg1

19 456

7 322

7 080

3,305

darklight_when_i_sleep_i_see_demons_(2016).scr

6 912

4 503

4 376

2,82

[ Mixedzx ]

chronos.bin

49 152

25286

23 530

6,944

bomb.tap

61 264

39 200

36 226

7,586

alterego.tzx

38 948

15 991

15 291

4,377

[ Music ]

ben_daglish_dark_fusion_(1988).ay

2 811

2 220

2 069

5,9

sairoos_inbetween_(2002).psc

10 048

2 712

2 500

7,817

fatal_snipe_machined.pt2

5 339

2 241

2 069

7,675

ksa_19_mng.w_(1996).stp

4 710

2 650

2 463

7,056

В заключение - если такой способ представления кодов переменной длины уже имел место, то значит я изобрёл велосипед. Ранее мне на глаза такое не попадалось.

Комментарии (2)


  1. DimPal
    24.05.2024 12:30
    +1

    Ну ход мылей то в целом веный. Но дело не в том каким способом указывать разрядность перед числом, а в том как это применить на практике. Если мы сжимаем массив чисел в котором есть частые значения, то записывать частые значения компактной последовательностью бит - это логично. Ну а дальше поиск компромиса - длина частых значений должна быть минимизирована (а для редких это уже не так важно), в идеале с учётом частоты использования. Тут есть простор для творчества можно комбинировать метод повторения нулей перед единицей как в кодах Элиаса, можно задать повторяющиеся нули/единицы и перед вторым нулём (например 0001110?????), можно брать двух, трёх (как у вас) и т.д фиксированные последовательности, а так же их комбинировать для разных диапазонов. А у Хафмана что там было? Вот только нюанс - это ещё не полноценный алгоритм сжатия, а его часть. Мне кажется обычно в этой части алгоритмов разработчики будут стараться использовать хорошо знакомые и известные решения.


    1. Zara6502 Автор
      24.05.2024 12:30

      Ну я и не пишу про алгоритм сжатия, я пишу про коды переменной длины и как вы выразились "Мне кажется обычно в этой части алгоритмов разработчики будут стараться использовать хорошо знакомые и известные решения" - я и пишу о том, что привычное и известное не всегда оптимальное.

      это ещё не полноценный алгоритм сжатия

      Касательно алгоритма сжатия статья будет только в том случае, если я хотя бы смогу сделать что-то сопоставимое с ZX0, иначе это не имеет смысла. Вариантов на тему LZSS на ретро-пространстве - ATARI, ZX и т.п - очень много, это и Aplib и LZ4 и Hrum/Hrust и т.п. (я ссылку дал на большую статью Introspec, там всё подробно). Для меня лично это просто хобби, постоянное наступание на грабли, изобретение велосипедов и т.п., не уверен что это кому-то будет интересно. С кодами, так уж получилось, эффективность очевидна, я напишу Эйнару, посмотрим, может это его заинтересует. Мне было бы приятно сделать свой вклад, для ретро машинок лишний килобайт лишним не бывает.