Введение

Алгоритм сжатия данных без потерь. Алгоритм цепи Маркова — Лемпеля — Зива (LZMA) — это алгоритм, используемый для выполнения сжатия данных без потерь. Он разрабатывался Игорем Павловым с 1996 или 1998 года и впервые был применён в формате 7z архиватора 7-Zip. Этот алгоритм использует схему сжатия на основе словаря, в некоторой степени схожую с алгоритмом LZ77, опубликованным Абрахамом Лемпелем и Джейкобом Зивом в 1977 году, и отличается высоким коэффициентом сжатия (обычно выше, чем у bzip2) и переменным размером словаря сжатия (до 4 ГБ), сохраняя при этом скорость декомпрессии, сопоставимую с другими широко используемыми алгоритмами сжатия. LZMA2 — это простой формат-контейнер, который может содержать как несжатые данные, так и данные, сжатые алгоритмом LZMA, возможно, с различными параметрами кодирования LZMA. LZMA2 поддерживает произвольно масштабируемое многопоточное сжатие и декомпрессию, а также эффективное сжатие частично несжимаемых данных.

Детали алгоритма декомпрессии

Кажется, не существует полной спецификации формата сжатия, представленной на естественном языке, за исключением той, что приведена в данном тексте. Описание ниже основано на компактном XZ Embedded декодере, разработанном Лассе Коллином и включенном в исходный код ядра Linux, из которого относительно легко можно вывести детали алгоритмов LZMA и LZMA2. Таким образом, хотя обращение к исходному коду в качестве справочного материала не идеально, любой программист сможет проверить достоверность приведенных ниже утверждений, потратив на это несколько часов.

Кодирование целых чисел в диапазоне

Диапазонный декодер также предоставляет средства для работы с битовым деревом, обратным битовым деревом и декодирования целых чисел с фиксированной вероятностью, которые используются для декодирования целых чисел и обобщают описанное выше однобитное декодирование. Для декодирования беззнаковых целых чисел, меньших заданного предела, предоставляется массив из (предел − 1) 11-битных переменных вероятности, которые концептуально организованы как внутренние узлы полного двоичного дерева с пределом листьев. Декодирование с использованием не-обратного битового дерева работает путем поддержания указателя на дерево переменных, начиная с корня. Пока указатель не указывает на лист, бит декодируется с использованием переменной, на которую указывает указатель, и указатель перемещается либо к левому, либо к правому потомку в зависимости от того, равен ли бит 0 или 1; когда указатель указывает на лист, возвращается число, связанное с этим листом. Таким образом, декодирование с использованием не-обратного битового дерева происходит от старшего к младшему биту, останавливаясь, когда в допустимом диапазоне возможно только одно значение (это концептуально позволяет использовать размеры диапазонов, которые не являются степенями двойки, хотя LZMA этим не пользуется). Декодирование с использованием обратного битового дерева, напротив, декодирует от младшего к старшему биту и, следовательно, поддерживает только диапазоны, являющиеся степенями двойки, и всегда декодирует одинаковое количество бит. Это эквивалентно выполнению декодирования с использованием не-обратного битового дерева с пределом, являющимся степенью двойки, и инвертированию последних битов результата. В функции в ядре Linux целые числа фактически возвращаются в диапазоне [предел, 2 × предел) (с добавлением предела к концептуальному значению), при этом переменная с индексом 0 в массиве не используется, а переменная с индексом 1 является корнем, а индексы левого и правого потомков вычисляются как 2i и 2i + 1. Эта функция вместо этого добавляет целые числа в диапазоне [0, предел) к переменной, предоставленной вызывающей стороной, где предел неявно представлен его логарифмом, и имеет собственную независимую реализацию из соображений эффективности. Декодирование с фиксированной вероятностью просто многократно выполняет декодирование битов с фиксированной вероятностью, считывая биты от старшего к младшему.

Детали алгоритма сжатия

Подобно ситуации с форматами декомпрессии, полной спецификации методов кодирования на естественном языке для 7-Zip или xz, насколько известно, не существует, за исключением той, что представлена в данном тексте. Описание ниже основано на кодировщике XZ для Java, разработанном Лассе Коллином, который представляется наиболее понятным среди нескольких переработок оригинального 7-Zip, использующих те же алгоритмы. Хотя ссылки на исходный код не являются идеальным решением, любой программист сможет проверить достоверность приведенных ниже утверждений, потратив на это несколько часов.

Структуры данных поискового словаря

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

Хашишистые цепи

Самый простой подход, называемый "хэш-цепочками", параметризуется константой N, которая может быть 2, 3 или 4 и обычно выбирается так, чтобы она была больше или равна размеру словаря. Он заключается в создании, для каждого k, меньшего или равного N, хеш-таблицы, индексированной кортежами из k байт, где каждое из ячеек содержит последнюю позицию, в которой первые k байт хешировались в хеш-значение, связанное с этой ячейкой хеш-таблицы. Связывание (chaining) достигается с помощью дополнительного массива, который хранит для каждой позиции словаря последнюю встреченную предыдущую позицию, первые N байт которой хешируются в то же значение, что и первые N байт рассматриваемой позиции. Для поиска совпадений длиной N или более поиск начинается с использованием хеш-таблицы размером N и продолжается с использованием массива хэш-цепочек; поиск останавливается после прохода по заранее определенному количеству узлов хэш-цепочки или когда хэш-цепочки "замыкаются", что указывает на то, что достигнута часть входных данных, которая была перезаписана в словаре. Совпадения размером менее N вместо этого находятся путем простого обращения к соответствующей хеш-таблице, которая либо содержит последнее такое совпадение (если оно есть), либо строку, которая хешируется в то же значение; в последнем случае кодировщик не сможет найти совпадение. Эта проблема смягчается тем фактом, что для удаленных коротких совпадений, использующих несколько литералов, может потребоваться меньше бит, а вероятность коллизий хешей в близлежащих строках относительно невелика; использование более крупных хеш-таблиц или даже таблиц прямого поиска может уменьшить эту проблему за счет увеличения числа промахов кэша и, следовательно, снижения производительности. Важно отметить, что все совпадения необходимо проверять, чтобы убедиться, что фактические байты совпадают в данный момент в конкретной словарной позиции, поскольку механизм хеширования только гарантирует, что в какой-то момент в прошлом были символы, хешировавшиеся в индекс ячейки хеш-таблицы (некоторые реализации могут даже не гарантировать этого, поскольку они не инициализируют структуры данных).

Двойные деревья

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

Кодировщик LZMA2

Кодировщик XZ LZMA2 обрабатывает входные данные блоками (размером до 2 МБ несжатых данных или 64 КБ сжатых данных, в зависимости от того, что меньше), передавая каждый блок кодировщику LZMA, а затем определяя, выводить LZMA2-блок с закодированными данными или LZMA2-блок без сжатия, в зависимости от того, что короче (LZMA, как и любой другой компрессор, неизбежно будет увеличивать размер некоторых типов данных, а не сжимать их). Состояние LZMA сбрасывается только в первом блоке, если вызывающая сторона запрашивает изменение свойств и каждый раз при выводе сжатого блока. Свойства LZMA изменяются только в первом блоке или если вызывающая сторона запрашивает изменение свойств. Словарь сбрасывается только в первом блоке.

Верхние кодирующие слои

Перед кодированием LZMA2, в зависимости от заданных параметров, xz может применять фильтр BCJ, который обрабатывает исполняемый код, заменяя относительные адреса на абсолютные, что повышает повторяемость данных, или дельта-фильтр, заменяющий каждый байт разностью между ним и байтом, находящимся перед ним. Параллельное кодирование выполняется путем разделения файла на блоки, которые распределяются между потоками, и каждый блок кодируется отдельно (например, с использованием блочного кодирования xz), что приводит к сбросу словаря между блоками в выходном файле.

Референтная реализация 7-Zip

Реализация LZMA, извлеченная из 7-Zip, доступна в виде LZMA SDK. Изначально она распространялась под двойной лицензией GNU LGPL и Common Public License, с дополнительным специальным исключением для связанных бинарных файлов, но 2 декабря 2008 года Игорь Павлов перевел её в общественное достояние с выходом версии 4.62. В настоящее время это метод сжатия по умолчанию для формата .7z, начиная с версии 9.30 от 26 октября 2012 года. Эталонная библиотека сжатия LZMA с открытым исходным кодом была первоначально написана на C++, но впоследствии портирована на ANSI C, C# и Java. В реализации 7-Zip используются различные варианты хеш-цепочек, бинарных деревьев и деревьев Патрисии в качестве основы для алгоритма поиска в словаре. Помимо LZMA, SDK и 7-Zip также реализуют множество фильтров предварительной обработки, предназначенных для повышения эффективности сжатия, от простого дельта-кодирования (для изображений) до BCJ для исполняемого кода. Также предоставляются и другие алгоритмы сжатия, используемые в 7z. Код, предназначенный только для распаковки LZMA, обычно компилируется примерно до 5 КБ, а объем необходимой оперативной памяти во время распаковки в основном определяется размером скользящего окна, используемого при сжатии. Небольшой размер кода, относительно низкие требования к памяти, особенно при использовании небольших длин словаря, и свободный доступ к исходному коду делают алгоритм распаковки LZMA хорошо подходящим для встраиваемых приложений.

Другие варианты реализации

В дополнение к эталонной реализации 7-Zip, формат LZMA поддерживают следующие программы. xz: потоковая реализация, включающая в себя инструмент командной строки, аналогичный gzip, поддерживающий как LZMA, так и LZMA2 в формате файлов xz. Благодаря высокой производительности (по сравнению с bzip2) и небольшому размеру (по сравнению с gzip), она получила распространение в ряде программных продуктов Unix-подобных систем. Fedora теперь использует xz для сжатия своих дистрибутивов. lzip: ещё одна реализация LZMA, предназначенная главным образом для Unix-подобных систем и являющаяся прямым конкурентом xz. Она отличается более простым форматом файла, что облегчает восстановление данных в случае ошибок. ZIPX: расширение формата сжатия ZIP, разработанное компанией WinZip, начиная с версии 12.1. ZIPX также может использовать различные другие методы сжатия, такие как BZip и PPMd.

ЛЖАМ

LZHAM (LZ, Хаффман, Арифметическое кодирование, Марков), – это реализация, подобная LZMA, которая жертвует скоростью сжатия ради очень высоких коэффициентов сжатия и более высокой скорости декомпрессии. Автор разместил её в общественном достоянии 15 сентября 2020 года.