11.02.2006

Архиватор LZH.

Введение.

В данном документе обсуждаются основные проблемы реализации алгоритма 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);

Словарь LZ77.

Словарь выполнен в виде байтового массива 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 не используются). Заметим, что подобный подход, в отличие от скажем блочно-адаптивного существенно зависит по скорости кодирования от избыточности данных, поскольку таблицы Хаффмана обновляются при кодировании каждого нового символа.

Основные проблемы.

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

  1. Дерево очень дорого в смысле поддержания, из-за необходимости обновления его структуры при обработке каждого символа. Кроме того, дерево не позволяет осуществить оптимизацию кодирования в плане выбора совпадения на минимальной дистанции.
  2. Адаптивное кодирование Хаффмана делает реализацию медленной на низкоизбыточных данных.
  3. Чтение-запись из потоков осуществляются побайтно, что увеличивает количество обращений к потокам.
  4. Словарь реально представляет окно из 4кб-60 символов, хотя декомпрессор и таблицы позволяют использовать ровно 4кб.

Возможные улучшения.

  1. Дерево допустимо заменить на более простую структуру типа хэш. Такая структура обладает рядом достоинств

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

  2. Адаптивное кодирование Хаффмана используется как в кодере, так и в декодере, поэтому отказаться от него невозможно без потери совместимости. В тоже время, существенно улучшить кодер сложно.
  3. Кэширование чтения/записи достаточно легко осуществить, выделив дополнительные объемы памяти (небольшие) под эти цели. При этом под сжатые данные логично выделить меньший объем, чем под оригинал. Интерфейс объекта подразумевает блочной обмен данными, хотя и не использует его, т.е. существенных изменений не потребуется.
  4. Для использования словаря в 4 кб достаточно изменить способ хранения еще не обработанной цепочки - проще всего ее хранить вне окна словаря, также возможно изменение подхода к зацикленности словаря.

Реализация.

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

  1. Произведена замена дерева на хэш. Выбран достаточно крупный хэш из 4096 элементов с функцией хэширования по 3 первым байтам цепочки H(D) = (D[2] xor (D[1] shl 4) xor (D[0] shl 8)) and $FFF. Такую функцию можно вычислять последовательно с меньшим числом операций, зная ее значение для предыдущего элемента. Введены массивы

    Таким образом, потребовалось 2 массива вместо 3 (в случае дерева).
    В удалении цепочек нет необходимости, т.к. можно определить окончание хэш цепочки (выход за пределы окна).
    Поиск по цепочке хэша по умолчанию ораничен 50ю незначимыми сравнениями (не принесших улучшения для кодирования, может быть изменено в поле MaxChain).

  2. В качестве словаря используется массив Dict. В отличие от оригинальной реализации словарь не зациклен и объединен с входным буфером размером 1кб (может быть изменен в тексте модуля). Массив Next используется циклическим образом для уменьшения объемов памяти. Упрощенно это работает так
  3. В качестве выходного буфера используется массив SBuf с размером по умолчанию 256 байт (можно изменить в тексте модуля).
  4. Структуры дерева Хаффмана и код их использующий весьма близки к оригинальным, поскольку улучшить их затруднительно. Кодирование дистанций, набор символов также остались без изменений.

Ленивое сравнение (Lazy matching).

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

Для реализации ленивого сравнения мы осуществляем подобную проверку с условием, что текущее совпадение достаточно короткое (не превышает значения поля MaxLazy - им можно регулировать соотношение скорость\сжатие), кроме того в функцию поиска передается пороговое значение равное длине текущего совпадения - это усколяет поиск. В случае, если найденное таким образом совпадение длиннее, мы отказываемся от кодирования текущего совпадения, а кодируем текущий символ. На следующем шаге поиск нового совпадения уже не нужен, однако может быть произведеная проверка следующей цепочки в рамках Lazy matching, если совпадение все еще не превышает MaxLazy.

Заключение.

В результате изменений возрасла скорость кодирования, на низкоизбыточных данных улучшение составило 1,5 - 2 раза (за счет адаптивного Хаффмана), а на высокоизбыточных может достигать до 10 - 15 раз (имеется ввиду обработка по типу память - память). Ухудшение сжатия наблюдалось на некоторых типах файлов (например, на некоторых bmp и dbf файлах), однако был и обратный эффект, объясняющийся увеличением окна словаря и оптимальностью выбора цепочек по дистанции. В среднем, при обработке большего количества файлов различных типов результат очень близок к оригинальному алгоритму.

Количество динамической памяти, необходимой для архивации составляет ~26 кб, в оригинальной реализации ~34кб. Использование стека исчисляется сотнями байт (если верить комментариям к оригинальному исходнику ~400) - никаких существенных изменений в этом плане не произошло.

Исходные тексты

Hosted by uCoz