11.02.2006
В данном документе обсуждаются основные проблемы реализации алгоритма LZH на примере известного кода Haruhiko OKUMURA & Haruyasu YOSHIZAKI в виде класса Delphi (портирован Kenji RIKITAKE, Douglas Webb, Danny Heijl). Алгоритм относится к семейству LZ77 со словарем 4кб и адаптивным кодированием Хаффмана на выходе. Необходимо также отметить, что и алгоритм и архиватор на данный момент представляют ограниченную практическую ценность из-за наличия гораздо более быстрых и совершенных реализаций, однако может быть использован в учебных целях и приложениях с низкими требованиями к количеству оперативной памяти.
Интерфейс объекта-архиватора фактически
сводится к 2м функциям архивирования и
разархивирования соответственно. Обмен
данными с входным и выходным потоками
осуществляется при помощи Callback функций
передаваемых в качестве аргументов. Также
при разархивировании необходимо
передавать размер реальных данных в
архивном потоке, тк архиватор не
поддерживает какого-либо заголовка архива (в
архивированном потоке нет никакой
служебной информации и нет символа-маркера
для обозначения конца архивного потока). Также никаким
образом не проверяется целостность архива.
...
PutBytesProc = procedure(var DTA; NBytes: WORD; var Bytes_Put: WORD) of object;
...
procedure TLZH.LZHPack(var Bytes_Written: LongInt; GetBytes: GetBytesProc; PutBytes: PutBytesProc);
procedure TLZH.LZHUnpack(TextSize: Longint; GetBytes: GetBytesProc; PutBytes: PutBytesProc);
Словарь выполнен в виде байтового массива
text_buf объемом порядка 4кб+(максимальная
длина строки) со скользящим указателем в
нем. Указатель "зациклен" и по
достижении конца словаря (4кб) он
возвращяется в начало. Максимальная длина
строки фиксирована и равна 60 - это значит,
что при поиске совпадений словаре,
максивальная длина этого совпадения
ограничена этим значением. Минимальная
длина совпадения - 3.
Примерный алгоритм следующий
Необходимость в словаре более 4кб
обусловлена удобством сравнения строк,
когда при сравнении в коце зацикленного
массива, поидее надо переходить в начало, но
этого можно и не делать, если хранить копию
начала массива после его конца (но это
требует и соответствующего заполнения
словаря).
Procedure TLZH.LZHPack(VAR Bytes_Written:LongInt; GetBytes:GetBytesProc;
PutBytes:PutBytesProc);
...
text_buf^[s] := ct;
IF (s < PRED(F)) THEN begin
text_buf^[s + N] := ct; // мы делаем копию
начала в область после конца
end;
...
Перед началом кодирования непосдедственно
предшествует указателю цепочка из 60
пробелов (#32).
Поиск - важнейшая часть LZ алгоритмов. В данном случае для организации поиска используется механизм бинарных деревьев. Деревья организованы в виде массивов lson, dad, rson. Вот некоторые особенности организации.
Кодирование символов производится с использованием адаптивного алгоритма Хаффмана, кодирование расстояний от указателя - статическими кодами разной длины, заданными таблично (эти коды обеспечивают более компактное представление коротких дистанций). В набор символов, кодируемых адаптивно, входят не только 256 значений байта, но и 58 значений длин совпадений (длина кодируется спец. символом, длины 1, 2 не используются). Заметим, что подобный подход, в отличие от скажем блочно-адаптивного существенно зависит по скорости кодирования от избыточности данных, поскольку таблицы Хаффмана обновляются при кодировании каждого нового символа.
В соответствии с выше изложенным можно указать несколько основных проблем реализации алгоритма, приводящих к весьма низкой производительности архиватора.
Единственный недостаток - хэш более чувствителен к входным данным чем дерево, и если используется ограниченный перебор элементов цепочек, то возможны потери в сжатии.
За основу улучшений были приняты положения, указанные в предыдущем параграфе. Основной задачей было кардинальное повышение быстродействия при сохранении совместимости с декодером.
Таким образом, потребовалось 2 массива
вместо 3 (в случае дерева).
В удалении цепочек нет необходимости, т.к.
можно определить окончание хэш цепочки (выход
за пределы окна).
Поиск по цепочке хэша по умолчанию
ораничен 50ю незначимыми сравнениями (не
принесших улучшения для кодирования,
может быть изменено в поле MaxChain).
Известный подход для улучшения степени сжатия алгоритмов LZ77. Заключается в том, что найденное совпадение не кодируется сразу, а выполняются дополнительные проверки. Наиболее простой вариант - проверить совпадение, так как если бы цепочка начиналась со следующего символа. Может оказаться, что следующая цепочка "лучше" - в этом случае имеет смыл отказаться от кодирования текущего совпадения, закодировать 1 символ и перейти к следующей цепочке.
Для реализации ленивого сравнения мы осуществляем подобную проверку с условием, что текущее совпадение достаточно короткое (не превышает значения поля MaxLazy - им можно регулировать соотношение скорость\сжатие), кроме того в функцию поиска передается пороговое значение равное длине текущего совпадения - это усколяет поиск. В случае, если найденное таким образом совпадение длиннее, мы отказываемся от кодирования текущего совпадения, а кодируем текущий символ. На следующем шаге поиск нового совпадения уже не нужен, однако может быть произведеная проверка следующей цепочки в рамках Lazy matching, если совпадение все еще не превышает MaxLazy.
В результате изменений возрасла скорость кодирования, на низкоизбыточных данных улучшение составило 1,5 - 2 раза (за счет адаптивного Хаффмана), а на высокоизбыточных может достигать до 10 - 15 раз (имеется ввиду обработка по типу память - память). Ухудшение сжатия наблюдалось на некоторых типах файлов (например, на некоторых bmp и dbf файлах), однако был и обратный эффект, объясняющийся увеличением окна словаря и оптимальностью выбора цепочек по дистанции. В среднем, при обработке большего количества файлов различных типов результат очень близок к оригинальному алгоритму.
Количество динамической памяти, необходимой для архивации составляет ~26 кб, в оригинальной реализации ~34кб. Использование стека исчисляется сотнями байт (если верить комментариям к оригинальному исходнику ~400) - никаких существенных изменений в этом плане не произошло.