Показаны сообщения с ярлыком Deflate. Показать все сообщения
Показаны сообщения с ярлыком Deflate. Показать все сообщения

среда, 1 сентября 2021 г.

Сжатие методом Хаффмана (Huffman Codes)

На картинке изображен метод кодирования Хаффмана, предложенный еще в 1952 году, 70 лет назад. Глядя на эту схему, приходит в голову сортированный список или стек(приоритеная очередь) с сортировкой по весовому коэффциенту.
Алгоритм построения кодов состоит из двух этапов. На первом этапе мы составляем и сортируем очередь указанным на картинке способом. На каждом шаге создаем композитный элемент очереди, включающий ссылки на два последних элемента и тем самым формируем из списка дерево -- еще одна динамическая структура. На втором этапе совершаем обход дерева, степень вложенности элементов в дерево будет давать длину кода. Данный метод используется для вычисления длин кодов, одновременно с эти можно вычислять сами коды, приняв направление ветвления, право - лево за ноль или единицу. Расчет кодов предлагается выполнять методом обхода дерева, а это -- рекурсия.

Привожу таблицу выданную моей реализацией алгоритма, алгоритм составлен только из созерцания картиники. Как?! -- Очередь с приоритетами и построение дерева. Коды можно считать несколькими способами, один из которых считается Каноническим.

No |wght|bits|code
 0 | 20 |  2 |00
 1 | 18 |  3 |010
 2 | 10 |  3 |011
 3 | 10 |  3 |100
 4 | 10 |  3 |101
 5 |  6 |  4 |1100
 6 |  6 |  5 |11010
 7 |  4 |  5 |11011
 8 |  4 |  5 |11100
 9 |  4 |  5 |11101
10 |  4 |  5 |11110
11 |  3 |  6 |111110
12 |  1 |  6 |111111

Мне не нравится предложенный алгоритм тем, что использует для сортировки кодов динамические структуры. Прежде всего хочется отказаться от динамических данных, чтобы исключить обращение к системе и сделать код быстрым. В крайнем случае, мы можем выделять место для списков из массива элементов (слайсами).

Как избавиться от дерева. Предлагаю не думать о дереве вовсе. Давайте представим, что мы не удаляем из очереди эдементы, а только добавляем композитные элементы с суммарным весом. Тогда проход по списку сверху вниз, с увеличением счетчика длины кода на каждом композитном элементе, дает длину следующих кодов. Алгоритм - мой, сам придумал, глядя на картинку. Но возможно за последние 70 лет, эта идея приходила не только мне.

Как избавиться от приоритетной очереди. От нее не избавиться, но мы могли бы использовать сортировку массива или создавать динамические данные из массива. Сортировка массива дает слишком много циклов и много операций копирования. В общем случае мы используем объект типа список из массива элементов и методы: "вставить с приоритетом"(list_insert_sorted), "получить следующий", и "выделить новый элемент из массива" (list_new). Используем процедуру обхода списка, в одном направлении -- односвязный список.

Это мы сейчас обсуждаем генерацию кодов, которая востребована на этапе упаковки данных, сжатия.

Декомпрессия выполнятся иначе. Для декомпрессии на вход подается таблица битовых длинн кодов, только третья колонка в нашем примере, вторая и четвертая колонки таблицы: частота использования и коды -- не заполнены.

В первой реализации метода декодирования (см. предыдущее сообщение), я использовал генерацию кодов из длин налету, без использования кодовых таблиц. Однако, теоретический максимум производительности такого метода - один бит за такт процессора. Медлено.

Метод декодирования с использованием дерева ветвления - тоже сравнительно медленный. Чтобы понять насколько медленно скажем так: символы основного алфавита кодируются длинами от 1 до 15 бит. Если разброс длин таблицы кодирования большой, то может понадобиться 15 циклов, чтобы найти нужный код. Однако, тут надо иметь ввиду, что длинные коды встречаются реже, чем короткие. Так что, берем оценку по среднему - восемь циклов на декодирование каждого символа.

Метод декодирования с использованием кодовых таблиц в теории будет работать быстрее, потому что разбор ведется сразу по несколько бит, в среднем по 8 бит для литералов и длин, и по пять бит для смещения. Т.е. ожидаем производительность табличных методов в несколько раз выше. Однако, для декодирования кодов с максимальной длиной 15 бит нужны таблицы декодирования размером в 32-128 кБайт, а их генерировать возможно дольше, чем разбирать поток по одному биту. По этой причине мы рассматриваем методы, которые используют частичное декодирование с помощью таблиц, а затем вычисляют остаток.

Ставим задачу: получить производительность декодирования - один цикл на символ, в среднем. Как это сделать. Для начала надо научиться по префиксу кода, находить индекс в таблице. Готовим коды с выравниванием по левому краю, как указано в таблице, чтобы можно было выполнять сравнение чисел.

 0 | 20 |  2 |00XXX
 1 | 18 |  3 |010XX
 2 | 10 |  3 |011XX
 3 | 10 |  3 |100XX
 4 | 10 |  3 |101XX
 5 |  6 |  4 |1100X
 6 |  6 |  5 |11010
 . . .

В таком виде можно вычислять индекс по кодам. Привожу алгоритм описанный в обзорной статье [ACM Comput. Surv., Vol. 1, No. 1, Article 1. Publication date: June 2019].

    1) Найти такое ℓ чтобы first_code_l[ℓ] ≤ code < first_code_l[ℓ + 1];
-- Коды упорядочены по убыванию частоты использования, найти какому диапазону принадлежит код.
    2) пусть offset ← (code − first_code_l[ℓ]) >> (L − ℓ);
-- находим смещение в таблице - номер символа. 
    3) Находим сам символ s ← first_symbol[ℓ] + offset;
Для поиска в таком режиме применяем следующий цикл, именно он будет определять производительность метода:
// Алгоритм поиска кода по таблице.
    set ℓ ← search_start[code >> (L − t)];
    while code ≥ first_code_l[ℓ + 1] do
        set ℓ ← ℓ + 1.

Я бы сказал так, выражаясь шаблонами: мы используем хеш таблицу для поиска кода.

Сначала из таблицы некоторой фиксированной длины мы выбираем величину минимальной длины кода (t). Затем подбираем код по таблице. Казалось бы выигрыша особенного нет, потому что получаем цикл от минимальной до максимальной длины кода.

Для отладки и исследования алгоритма я ввел еще одну таблицу - search_end - максимальная длина кода с заданным префиксом, которая ограничивает глубину поиска кода.

// Предложенный алгоритм поиска
    set ℓ ← search_start[code >> (L − t)];
    set u ← search_end  [code >> (L − t)];
    while (ℓ < u) && code ≥ first_code[ℓ + 1] do
        set ℓ ← ℓ + 1.

Тут надо принять во внимание, что значения в таблице search_strart (минимальная длина кода) и search_end (максимальная длина кода) для данного префикса чаще всего совпадает, и мы статистически за одну проверку условия (ℓ < u) получаем битовую длину и сам код, без множества итераций. Поиск по таблице можно было бы оптимизировать с использованием бинарного поиска, но это не сработает, потому что коды статистически так распределены, что их проверять лучше последовательно.

Табличный метод декодирования может дать результат по производительности заметно выше, чем декодирование по дереву. Табличный метод должен иметь возможность ограничения размера таблиц декодирования. В представленном методе видно, что параметр t можно выбрать произвольной длины, больше минимальной длины кода, тем самым повысить эффективность. Хвост разложения тоже можно считать по таблице в два каскада... в три каскада...

И тут пришла в голову мысль, а не посмотреть ли как другие решают эту задачу. Посмотрел. Опять нашел код с исходниками Марка Адлера и отказом от права контролировать копии, с 1996 года в составе GNU, gzip -- этот код есть на каждом сервере. Код тридцатилетней давности, 30 лет, в коде упомянаются проблемы компиляции под MSDOS и OS/2, с ипсользованием Turbo C. В файле ТОДО рекомендуется использовать оптимизации CRC от Росса Вильямса, который уже пятнадцать лет, как на пенсии.

Есть давно забытое искусство написания декодеров, по принципу "магического кристалла": крутим-вертим кристалл, а из него выпадают: то форматированные данные, то инструкции, как их исполнять - декодер команд. Давайте представим, что наш формат -- это поток инструкций для некоторого процессора, которые надо декодировать. Для декодирования используется конечный автомат - переход по состояниям, и таблицы - таблицы - таблицы. При этом декодирование отделено от исполнения команд, рассматриваем декодер, как устройство или функцию, которая из одного потока данных делает другой поток данных иначе сформатированный и выровненный. Попробуем представить не то, как оптимизировать существующий алгоритм, а как описать произвольный табличный декодер минимальным числом инструкций.

// Алгоритм декодирования
    do{
        t ← t.data + (взять_из_потока(t.n_bits) & маска_выборки(t.extra)) 
    } while нужно_продолжить(t.extra);
    сдвинуть_поток(t.n_bits);
    выполнить_обработку_данных(t.data, t.extra).

Примерно такой шаблон кода должен решать задачу декодирования. Более того, если этот фрагмент выполняется в цикле, таблицами (t) и переходами внутри таблицы (t.data) можно организовать контектстно-зависимый разбор. Маска выборки может быть линейная или не линейная. Допустим нам нужно декодировать программу из системы команд одного процессора в систему команд другого процессора. Или скажем, нужно написать итератор по системе команд Intel x86, т.е. по префиксам выделять длину инструкции -- тот же алгоритм декодирования, тот же шаблон. Или понадобилось разбирать синтаксис и грамматику в интерпретаторе команд, - тоже самое.

Конкретно, нам нужно выбрать инструкцию, на которой будет все работать. Для пионеров можем предложить реализацию на инструкции Intel AVX512_BITALG:

Synopsis
   __mmask16 _mm_bitshuffle_epi64_mask (__m128i b, __m128i c)

Instruction: vpshufbitqmb k, xmm, xmm
CPUID Flags: AVX512_BITALG + AVX512VL

Т.е на одной такой инструкции можно декодировать входной поток с использованием перестановки бит. Комбинация бит во входном потоке приводит к переходу по таблице декодирования. Результат - маска.

Мы выбираем простой способ формирования масок ((1<<(n))-1), который можно реализовать в одну инструкцию Intel BMI или BMI2;

В зависимости от целевой платформы, мы можем с таким алгоритмом достичь производительности в один такт(цикл) на кодовое слово. И тут происходит основное расстройство - мы живем в золотом веке программирования - этот подход уже реализован в gzip. С той лишь разницей, что реализован он средствами 30 летней давности, в инструкциях ANSI С. Более современная интерпретация алгоритма пристуствует в проекте zlib-ng.

Это сильно расстраивает, но наша цель -- предложить самый быстрый и компактный алгоритм декодирования.

Я потратил выходные на то чтобы научиться составлять таблицы декодирования и подобрать функцию декодера. И вот что получилось.

// Алгоритм декодирования
#define BIT_MASK(n) ((1<<(n))-1)

    t = t + (stream & BIT_MASK(N));
    if (t->extra) {
        t = te + t->n + ((stream>>N) & BIT_MASK(t->extra));
    }
    stream >>= t->bits;
    return t->n;

Функция использует две таблицы: (t) - основаная с разрядностью индекса N и дополнительная (te) - для декодирования кодов с длиной больше N. Декодирование выполняется не более чем в два этапа. На выходе алгоритм выдает декодированное значение. Параметр N выбирается разный для таблиц декодирования литералов (N=8) и для декодирования дистанций (N=5). При данных значениях параметра вторая таблица используется редко. Операции извлечения бит из потока могут выполняться с использованием инструкций Intel BEXTR (BMI1). Полагаю данная реализация является оптимальной с точки зрения производительности.Если при этом удасться сохранить лаконичность кода, то и самая быстрая в своем роде (быстрее gzip, zlib и zlib-ng).

Суммараная производительность алгоритма будет складываться из времени подготовки таблиц, времени работы алгоритма декодирования (inflate), и времени подгрузки данных.

четверг, 29 июля 2021 г.

Deflate -- собственная реализация

Я пытался осознать как написан zlib, чтобы встроить картинки PNG в контроллер. Не понравилось, взялся за реализацию из RFC 2083 (PNG) и RFC 1951 (Deflate). Задача - реализовать Deflate с таблицами Хаффмана для распаковки изображений PNG. Интерсно, сколько Марку Адлеру лет. Нашел, что он проявлял активность в проекте zlib-ng еще совсем недавно. Марк Адлер (со)автор таких значимых форматов и утилит, как: zlib, deflate, brotli, zopfli, pigz. И все равно пишет не читабельный код. Итак, приступим. Базовая операция в формате Deflate, на которую завязано быстродействие - это чтение из битового потока по одному биту. Производительность теоретическая, которую можно на этом получить - один такт на бит. После чтения из потока одного бита выполняется поиск кодовой последовательности по дереву бинарному. Это вам повезет найти в реализации zlib бинарное дерево. Я не нашел. Захотел реализовать свою концепцию поиска в дереве двоичных кодов, без дерева.
// Чтение кодов из потока
uint32_t huffman_read_bit(uint32_t code)
{
	code = (code<<1) | (stream&1);
	stream >>= 1;
	return code;
}
Эту операцию можно оптимизировать с использованием инструкций Intel BMI и BMI2, или с использованием сдвига через флаг переноса CF.
#include <intrin.h>
code = (code<<1) + _bittest(src, offset++);
Например, такая операция с использованием псевдофункции дает производительность в цикле ровно один такт на бит. Можно ускорить работу алгоритма, если выполнять операция разворачивания бит оптом, сразу на всем потоке, просто идея.

// Алгоритм поиска кодов Хаффмана без кодовых таблиц.
uint32_t huffman_decode(uint8_t *bl_count, int max_bl)
{
    uint32_t bt_code=0, bt_offset=0, code=0;
    int i=0;
    for(i=0; i<=max_bl; i++, bt_code <<= 1) {
        code = huffman_read_bit(code, 1);
        if (bl_count[i]==0) continue;
        if (code - bt_code < bl_count[i]) break;
        bt_code   += bl_count[i];
        bt_offset += bl_count[i];
    }
    return bt_offset + (code - bt_code);
}
Алгоритм возвращает индекс в упорядоченном по кодам Хаффмана массиве представляющем собой алфавит кодов. На каждом шаге алгоритма надо выбирать один бит и совершать ветвление в дереве. Опереации с деревом оказались не нужны, все основано на вычислении индекса в таблице алфавита в соответствии с кодом. Натолкнула на мысль о вычислении индекса в массиве методика расчета начальных кодов с заданной разрядностью, которая представлена в исходном коде Deflate. Массив bl_count содержит на входе число длин кодовых последовательнстей, длина выступает индексом.
// расчет кодов Хаффмана
for (i = 1; i < MAX_BITS; i++) {
        next_code[i] = code;
        code = (code + bl_count[i]) << 1;
}
next_code[i] = code;
В оригинальной реализации обнаружил функцию разворота порядка следования бит и составление таблиц в человеческом представлении, порядок в таблице по номеру символа. В моей таблице порядок - по коду Хаффмана. Разворачивать индексы, в отличие от исходного Deflate, в нашей реализации не понадобилось. Исходный код генерации таблиц: gen_codes.
Пример алфавита и кодов
   Литерал/длина/ код
         0:  6, 3c
         1:  0, 0
         2:  7, 7c
         3:  7, 7d
         4:  4, c
         5:  5, 1c
         6:  5, 1d
         7:  4, d
         8:  2, 0
         9:  2, 1
        10:  3, 4
        11:  7, 7e
        12:  6, 3d
        13:  0, 0
        14:  0, 0
        15:  0, 0
        16:  3, 5
        17:  7, 7f
        18:  0, 0
В нашем случае алфавит укладывается по порядку кодов, коды с длинами 0 не используются. Порядок определяется значением кода. Сначала идут коды с меньшей разрядностью, потом с большей.
по порядку кодов: 8 9 10 16 4 7 5 6 0 12 2 3 11 17
Ниже привожу мой алгоритм распаковки Deflate с использованием динамических таблиц. Хотя, самих таблиц, как и дерева бинарного поиска, тут нет. Функция huffman_decode вычисляет налету индекс в алфавите литералов, длин и смещений.

while(src<s_end){
    uint32_t code = huffman_decode(cwl_count, max_cwl);
    code = cwl_alpha[code];// Алфавит литералов и длин
    if (code < 256){// Декодирование литерала
        *dst++ = code;
    } else {
        if (code==256) // код завершения
            break;
        // Декодирование длины по основному алфавиту
        int mlen = fixed_length_mlen [code-257]+3;
        int extra= fixed_length_extra[code-257];
        if (extra){ // чтение бит из потока
            mlen += stream_read_bit(extra);
        }
        // Декодирование смещения по алфавиту смещений
        code = huffman_decode(dl_count, max_dl);
        code = dl_alpha[code];// Алфавит смещений
        uint32_t offset= fixed_distance_offset[code]+1;
        extra = fixed_distance_extra [code];
        if (extra){
            offset += stream_read_bit(extra);
        }
        {// фукнция копирования подстроки
            uint8_t * s = dst-offset;
            int i;
            for(i=0; i < mlen; i++)
                *dst++ = *s++;
        }
    }
}
Исходный код на Github
Очень помогло подробное описание разбора потока Deflate, спасибо автору. Его пример использовал для отладки алгоритма, в качестве тестового вектора.

среда, 7 июля 2021 г.

LZO, LZ4, LZF, Deflate vs LZJB -- упаковка данных на потоке

Занимаюсь осмыслением файловой системы для контроллеров.., а Вася решает олимпиадные задачи. Вася задал вопрос, как эффективно находить подстроки. Я вспомнил идею из алгоритма LZJB, и увлекся созерцанием этого алгоритма. LZJB - алгоритм потоковой упаковки данных, был включен в дистрибутива файловой системы ZFS. Найти исходники LZJB можно в дистрибутиве OpenZFS или OpenIllumos, клона OpenSolaris. Алгоритм изначально был предназначен для быстрой записи дампа памяти, в случае краха системы. От него ничего не требуется, кроме скорости записи и простоты чтения. Эффективно дложны паковаться длинные последовательности ноликов и высказывания типа 0xBAADF00D и 0xDEADBEEF - неинициализированная память, остальное не так важно. Подстроки алгоритм ищет через хеш-функцию недоделанную. Не могу сказать, что упаковка данных может оказаться полезной, но с точки зрения проектирования файловой системы, опция упаковки, как в ZFS или как в SquashFS, не помешает. Осмыслить возможность стоит.

Алгоритм простой и в тоже время невероятно криво написан, не глючит каким-то чудом. Когда-то лет десять или более назад, когда открыли исходники OpenSolaris, мне понравились некоторые идеи и в том числе засматривался на алгоритм потокого сжатия LZJB, для применния в своих разработках. Сейчас вижу, кривой, хочу подправить, чтобы не был таким кривым.

Первая проблема- таблица хеш не использует второй уровень сравнения, его просто нет, можно найти подстроку только одного вида, последнюю, которая соответствует хешу.

Вторая - подстроки от вставленных подстрок не считаются, по какой-то причине получается, что часть шаблонов игнорируется.

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

Взял за основу хеш таблицы и способ работы со сылками из Elf - формата.

Простая замена в алгоритме хеш таблиц позволяет существенно увеличить степень сжатия. Кроме того, ввел в алгоритм заполнение хеш таблиц от всего потока, а не только от фрагментов некодированных, как в исходном алгоритме.

В основе кодирования LZJB лежит эффективный алгоритм подгрузки и декодирования битовых строк, отвечающих за выбор формата кодирования. Я пробовал заменить его на кодирование подстрок наподобие форматов кодирования LZO. В первом приближении способ работы с флагами "copymap" оказался эффективнее. Флаги выбора типа кодирования дают накладные расходы 1 бит на байт, который не входит в подстроку. Подстрока кодируется двумя байтами: длина последовательности и смещение в буфере от текущей позиции записи. Формат упаковки длины и смещения используется только один:6 бит длина, 10 бит - смещение.

Чтобы понять насколько эффективный при этом получается декодер, предлагаю рассмотреть функцию декодирования в виде шаблона:

// Алгоритм распаковки LZJB.
int lzjb_decompress(...)
{
	uint8_t copymap = 0;
	int copymask = 0x80;

	while (dst < d_end) {
		/* выбор типа кодирования */
		if ((copymask <<= 1) == 0x100) {
			copymask = 1;
			copymap = *src++;
		}
		if (copymap & copymask) {
			/* 1:декодирование формата ссылки */ 
			/* 2:копирование подстроки */ 
		} else {
			*dst++ = *src++;
		}
	}
	return (0);
}

Декодирование однопроходное, без вложенных циклов и сравнимо по быстродействию с операцией копирования. Предложен интересный способ декодирования формата с подгрузкой битовых строк "copymap". Приведенного алгоритма достаточно чтобы понять как все устроено, не буду приводить код целиком.

Возможно ли из этого алгоритма сделать что-то превосходящее по параметрам. --ДА! Во первых мы можем предложить несколько форматов упаковки подстрок, в частности тут последовательности меньше 3 байт не кодируются, а мы можем, если ввести формат умещающийся в один байт. Исходный алгоритм (LZJB) использует максимальную дистанцию поиска подстрок 1024 байта. Мы можем приспособить для работы с блоками размером 4кБайт, если увеличим эту дистанцию хотя бы до 2кБайт. Проверил, оказалось действительно на блоках размером 4кБайт эффективнее работает дистанция в 2кБайта. Второй формат должен быть скромнее, он должен паковать маленькие последовательности, опытным путем вывел формулу второго формата: 2-бита на длину, 6-бит смещение. Получаем два формата: "2-6" и "5-11" -- их можно сделать параметрическими для случая, когда размер блока флеш памяти меньше 4кБайт. Я пропущу описание дня работы, когда я изучал распределения и эффективность упаковки двумя и более форматами. Для отладки процесса кодирования использовал недоделанный кодировщик, который не заботится о записи, а только высчитывает длины и смещения для выбранного способа кодирования. Для оценки эффективности использовал различные файлы: картинки, тексты программ, бинарники x86 и ARM, форматы XML. Да наибольшая эффективность достигается именно на форматах типа XML, на векторной графике SVG в частности.

Разработка функции декомпрессии

Важно отметить, что разработка алгоритма начинается не с функции сжатия, а со структуры алгоритма распаковки, как он должен выглядить для максимальной производительности. В исследовательских работах есть своя прелесть, нет никакого тех задания пиши, что хочешь. Но какова цель? Ставлю цель, чтобы алгоритм работал быстро, чтобы распаковка была эффективной. Быстро - означает возможность использования векторных инструкций для копирования и сравнения. Идеально было бы избавиться от циклов при поиске подстрок в алгоритме сжатия и от проверок и вложенных циклов в алгоритме распаковки. Чтобы избавиться от циклов при поиске, предлгаю ввести ограничение, длина подстроки не может предвышать 64 или 32 байта, -- этот параметр завязан на формат кодирования. Для формата "6-10" (длина-смещение) подойдет ограничение длины подстроки в 64=2^6 байта, а для формата "5-11" ограничение длины подстроки в 32=2^5 байта. Чтобы можно было переключаться между двумя(тремя) форматами кодирования придумал двухуровневую структуру, на основе исходного алгоритма декодирования.

// Структура алгоритма распаковки.
int my_decompress(...)
{
    uint8_t copymap = 0;
    int copymask = 0x80;

    while (dst < d_end) {
        /* выбор типа кодирования */
        if ((copymask <<= 1) == 0x100) {
            copymask = 1;
            copymap = *src++;
        }
        if (copymap & copymask) {// флаг кодирования
            if ((copymask <<= 1) == 0x100) {
                copymask = 1;
                copymap = *src++;
            }
            if (copymap & copymask) {// флаг формата
                /* 1.1:декодирование формата №1 2-6 */ 
            } else {
                /* 1.2:декодирование формата №2 5-11 */ 
            }
            /* 2:копирование подстроки */ 
        } else {
             *dst++ = *src++;
        }
    }
    return (0);
}

Мой алгоритм декомпрессии использует битовый поток для выбора форматов кодирования. Еще короче структуру алгоритма можно выразить следующим образом.

// Структура алгоритма распаковки.
int my_decompress(...)
{
    while (dst < d_end) {
        /* выбор типа кодирования */
        if (bitstream_get(stream, 1)) {// флаг кодирования
            if (bitstream_get(stream, 1)) {// флаг формата
                /* 1.1:декодирование формата №1 2-6 */ 
            } else {
                /* 1.2:декодирование формата №2 5-11 */ 
            }
            /* 2:копирование подстроки */ 
        } else {
             *dst++ = *src++;
        }
    }
    return (0);
}

Результат. При сжатии фала SVG получил степень сжатия 0.42 алгоритмом LZJB и степнь сжатия 0.27 моим алгоритмом. Мой алгоритм потокового сжатия около 90 строк, алгоритм распаковки =34 строки. Добавить перцу: в моем алгоритме хеш функция берет первый байт последовательности, обработка не требуется, эффективнее некуда. Приведу функцию заполнения хеш таблицы, остальное следует из описания алгоритма. Хеш таблицы для потокового сжатия не используют динамические данные или списки. Список формируется в массиве "chain".

// Функция заполнения хеш-таблицы
uint16_t _htable_insert(uint16_t *htable, uint8_t key)
{
	uint16_t *chain = htable + 256;
	uint16_t y = n_chain++;
	uint16_t *head = &htable[key];
	chain[y] = *head;
	*head = y;
	return y;
}

При поиске подстроки максимальной длины используется цикл по цепочке "chain", с массивами можно работать, как со списками.

// цикл поиска подстроки с использованием хеш-таблицы
hash = *src;// это моя хеш-функция, копирует байт.
i =_htable_insert(htable, hash);
while ((i = chain[i])!=0) 
{// обработка подстрок по цепочке
  . . .
}

Исходники на Github

Выбор хеш-функции

Исходно утверждал, что результат не должен зависеть от выбора хеш функции. Однако, это смотря в чем измерять результат. Степень упаковки действительно не должна зависеть от выбора хеш- функции. Однако, выбор влияет на скорость запаковывания, сколько требуется циклов на поиск значения в хеш-таблице. Для сравнения предлагаю несколько вариантов хеш функции. Все они основаны на том, что в алфавтие примерно 32 буквы чего-то значат и часто применяются, в таблице символов используется только половина (7 бит или 6 бит), остальная половина используется для кодирования чего-то другого, другого языка. Эта идея годится для английского языка и ASCII таблиц. Чтобы высчитывать хеш от текстов UTF-8 надо применять знания о кодировании UTF-8. Последовательности кодирования имеют вид 110xxxxx 10xxxxxx .. Видно, что значимыми бывают 5 бит первого байта и 6 бит последующих.

// Функция хеш из LZJB
static uint32_t lzjb_hash(uint8_t* src, int min_len)
{
	uint32_t hash;
	hash = (src[0] << 8) + src[1];
	hash += hash >> 9;
	hash += hash >> 5;
	return hash;
}
// Функция хеш из GLIB для строк
static uint32_t glib_hash(uint8_t* src, int min_len)
{
	uint32_t hash;
	hash = (src[0] * 33) + src[1] + 5381;
	return hash;
}
// Оптимальная хеш
static uint32_t my_hash(uint8_t* src, int min_len)
{
	uint32_t hash;
	hash = (src[0] * 66) + src[1];
	return hash;
}
// Функция Фибоначчи хеш
static uint32_t lz4_hash(uint8_t* src, int bits)
{
	uint32_t val = (uint32_t) src[0] | ((uint32_t) src[1] << 8);
	return (val * 2654435761ULL) >> (32-bits);
}

Первый вариант взят из оригинального алгоритма LZJB, адаптирован для двух-байтвых последовательностей. Второй из описания работы хеш функции glib, g_str_hash. Третий подбирал по мотивам двух предыдущих. Кроме того пробовал использовать остатки от деления на простые числа, что тоже благотворно сказывается на равномерности распределение хешей по таблице. Для отладки выводил среднее число циклов при поиске в хеш таблице. Отлаживал Хеш функцию на текстах XML и русскоязычных текстах в кодировке UTF-8. Компромисом оказалась функция my_hash. Почему так, не скажу, числа подбирал исходя из идей кодирования.

Повторюсь, хороший результат получается, для функций содержащих остаток от деления на простое число. Функция хеширования может быть составлена из комбинации сдвигов, побитовой операции XOR и умножения на некоторое обратное число. Остаток от деления на простое число можно представить операцией умножения на обратное число. Пример такой хеш функции нашел в реализации алгоритма сжатия LZ4. Использование lz4_hash с параметром bits=16 дает сравнимый результат.

Сравнение методов кодирования форматов

Изучив несколько вариантов фукнций сжатия на потоке, среди которых LZ4, LZO, LZF, пришел к выводу: все они одинаковые по сути, различаются только методы кодирования последовательностей (litterаl/RLE-run length encode) литерал/длина последовательности копирования. В моем варианте алгоритма выбор формата RLE - это битовые последовательности, а применяется всего два формата кодирования. В остальных рассматриваемых форматах LZO, LZF, LZ4 методы кодирования схожи, но используют первый байт кодового блока для выбора метода кодирования, включая выбор последовательности литарал или RLE. Алгоритм компрессии можно разложить на два этапа: 1) составление таблицы, включающий три поля для кодирования (длина литерала, длина последовательности, смещение в выходном буфере), и 2) функция кодирования по выбору.

Степень сжатия при одинаковой функции поиска повторяющихся фрагментов определяется исключительно эффективностью формата кодирования. Наш метод кодирования оказался самым эффективным. Для доказательства этого утверждения я использовал расчет кодовых длин для каждого из форматов, на всех тестовых. Например, исходный формат кодирования LZJB не позволяет кодировать бвухбайтовые последовательности - это существенно снижает его эффективность и напрямую влияет на степень сжатия, используется всего один вариант формата кодирования. В LZO содержится много вариантов кодирования. В LZF - два варианта кодирования, в LZ4 - несколько, за счет расширений, метод расширения не кажется эффективным. Существенно выделяется формат LZJB, потому что для выделения литералов используется не кодовое слово, а отдельный механизм дающий прибавку +1 бит на литерал. Такой подход оказыватеся эффективнее судя по степени сжатия.

При проектировании фукнции сжатия на эффективность влиют несколько факторов: 1) Использование словаря -- мы пока не пытаемся встроить использование словаря, может зря, 2) кодирование последовательностей несколькими форматами, включая двухбайтовые. 3) Особый метод кодирования для снижения накладных расходов при кодировании литералов.

Ниже привожу краткие характеристики форматов кодирования:

LZJB: максимальная дистанция обратного просмотра буфера 1кБайт (10 бит). 1 формат 2 байта.

0|S(8) -- кодирование литералов 1 байт+1бит.
1|L(6)|D(10): length=L+3, distance=D; -- кодирование RLE 2 байта+1бит

LZO: максимальная дистанция 2кБайта (11 бит)

01L DDD SS | H(8) -- копирует L+3 байт (3..4), и S литералов. 
1LL DDD SS | H(8) -- копирует L+5 байт (5..8), и S литералов. 
LLL DDD SS | H(8) -- копирует L+3 байт (3..10), и S литералов. 
. . . есть и другие варианты.
	distance = (H << 3) + D + 1

LZF: максимальаня дистанция 8кБайт (13 бит), два формата

T1(3)|T2(5) - токен, первый байт кодового блока
при T1=0, n_literal= T2+1 -- копирование литералов
при T1=7, length = T1+2+L(8), distance = (T2<<8) + D(8)+1; -- кодовый блок 3 байта
length = T1+2, distance = (T2<<8) + D(8)+1; -- кодовый блок 2 байта

LZ4: максимальная дистанция 2кБайта (11 бит)

T1(4)|T2(4) - токен, первый байт кодового блока
при T1=0, n_literal= T2+1 -- копирование литералов
-- кодовый блок 2 байта, расширяемый

LZJB-2, Мой вариант: максимальная дистанция 2кБайта (11 бит).

0| S -- кодирование литералов 1 байт+1бит
10|L(2)|D(6) : length=L+2, distance=D+1; -- кодовый блок 1 байт и 2 бита
11|L(5)|D(11): length=L+3, distance=D+1; -- кодовый блок 2 байта и 2 бита
при L=31     : length=L(8)+34, distance=D+1; -- кодовый блок 3 байта и 2 бита

Коды Хаффмана

Не смог обойти вниманием коды Хаффмана. Основная причина, по которой я привожу кусок кода алгоритма декодирования Defalte [RFC 1951] - не нашел читабельной реализации, чтобы из кода было понятно, как выполняется кодирование. Из текста RFC не сильно понятно, как реализовать таблицы кодирования, хотя расписано все достаточно подробно. Красивой реализации на три строчки кода, чтобы из кода было понятно, как выполняется разбор формата - не нашел. Предлагаю свою реализацию:

// literal/length decode
code = huffman_read_bit(0, stream, 5);
switch (code & 0x1F) {// 5-bit prefix
case 0b00000 ... 0b00101:// 256 - 279
    code = huffman_read_bit(code, stream, 2);
    code = code - 0b0000000 + 256;
    break;
case 0b00110 ... 0b10111://   0 - 143
    code = huffman_read_bit(code, stream, 3);
    code = code - 0b00110000 + 0;
    break;
case 0b11000 ... 0b11000:// 280 - 287
    code = huffman_read_bit(code, stream, 3);
    code = code - 0b11000000 + 280;
    break;
case 0b11001 ... 0b11111:// 144 - 255
    code = huffman_read_bit(code, stream, 4);
    code = code - 0b110010000 + 144;
    break;
}

Данынй фрагмент написан на языке Си, как ни странно, с использованием бинарных констант и диапазонов case. В отличие от представленных ранее методов кодирования, код Хаффмана не выровнен на байт, а все биты берет из потока. Функция чтения бит из потока, представлена huffman_read_bit() и реализована таким образом, чтобы дополнять кодовые последовательности битами из потока.

Глядя на фиксированную таблицу кодирования Delate, и сравнивая с представленными методами кодирования LZJB-2, понимаю, - мой метод в чем-то лучше кодирует, по одной причине - он использует двухбайтовые последовательности, а метод fixed Delate вообще зря теряет байт на кодировании длины. Метод кодирования LZJB использует два источника: поток бит и поток байт - это выигрышная стратегия. Вероятно выиграть в степени сжатия можно с упаковкой форматов кодирования полей длина-смещение (RLE) в битовый поток.

Данынй фрагмент кода представляет собой кодирование литерала или длины последовательности методом Хаффмана с фиксированной таблицей кодирования. Таблица кодирования может быть задана динамически и представлена, как бинарное дерево, где каждое ветвление выполняется по старшему биту.