Как я могу оптимизировать свой компрессор с раздвижным окном Lz77?

Я написал компрессор Java для очень непонятного формата сжатия. (В основном он использовался на компьютерах Amiga в 1990-х годах).

Существует довольно много документации о том, как распаковывать формат файла, но нет ни одной о том, как его сжимать.

Итак, я попытался сделать это сам. Работает, но есть одна проблема. Мне требуется 42 секунды, чтобы сжать все файлы, которые я хочу сжать, с «настройками низкой интенсивности». Это занимает примерно в 10 раз больше времени при более высоких настройках интенсивности.

Я считаю, что это может быть намного быстрее, чем это.

По сути, это вариант с раздвижным окном Lz77.

Настоящим узким местом является поиск существующего экземпляра для сжатия. Прямо сейчас я использую Map<Byte, List<Integer>> (List<Integer> — это все индексы, в которых присутствует байт.)

Чтобы найти потенциальное совпадение, он делает следующее:

Он принимает текущий индекс сжимаемого файла. Он получает List<Integer> из карты с байтом по текущему индексу.

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

Я думаю, что лучшая структура данных может значительно ускорить это, но я застрял на этом этапе.

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

Как я могу оптимизировать сжатие, не делая его менее эффективным при упаковке данных?

Основной код узкого места:

private static int search(byte[] data, int bufferEnd, List<Byte> target, Map<Byte, List<Integer>> dictionary) {
    int minIndex = Math.max(0, bufferEnd - getMaximumOffset(target.size())); // There's a certain point at which data will not be compressed. By calculating it here, it saves a lot of overheard, and prevents this from becoming O(n^2)

    byte test = target.get(0);
    if (!dictionary.containsKey(test))
        return -1; // No results found.

    List<Integer> possibleResults = dictionary.get(test);

    for (int i = possibleResults.size() - 1; i >= 0; i--) {
        int testIndex = possibleResults.get(i);
        if (minIndex > testIndex)
            break; // We've gone too far.

        // Test this
        boolean pass = true;
        for (int j = 1; j < target.size(); j++) {
            if (target.get(j) != data[j + testIndex]) {
                pass = false;
                break; // Break from the j for loop.
            }
        }

        if (pass) // A match has been found. Return it.
            return testIndex;
    }

    return -1;
}

Который вызывается:

while ((tempIndex = search(data, i, searchList, dictionary)) >= 0) { // Find the longest compressable bunch of characters.
    if (data.length - 1 == readIndex) // If we've reached the end of the data, exit.
        break;

    searchList.add(data[++readIndex]);
}

Полный код здесь для кому это нужно.


person Petersnow2    schedule 01.10.2018    source источник
comment
Это может быть лучше изложено на codereview родственного сайта.   -  person KevinO    schedule 01.10.2018
comment
Хорошая точка зрения. Не знала о существовании этого сайта, спасибо. Вместо этого я опубликую его там.   -  person Petersnow2    schedule 01.10.2018
comment
Map<Byte, List<Integer>> можно записать как int[][] (с некоторым сопоставлением байтов с целыми числами для первого индекса) (если только вам не нужны нулевые ключи или элементы в списках значений), что позволяет избежать автоупаковки.   -  person Andy Turner    schedule 01.10.2018
comment
Пожалуйста, посмотрите мое редактирование о том, как встроить встроенный код.   -  person maaartinus    schedule 01.10.2018
comment
Что вы, вероятно, хотите (компромисс скорости/памяти), так это не хранить простую плоскую таблицу поиска, а скорее построить дерево префиксов (trie), чтобы у вас был быстрый поиск самого длинного префикса.   -  person Hiery Nomus    schedule 01.10.2018
comment
@HieryNomus Спасибо за предложение! Я кое-что прочитал в Trie, но либо я не понимаю его правильно, либо это не совсем то, что я ищу. Проблема в том, что мне нужно иметь возможность взять любой байтовый подсписок, который был создан ранее, и мне нужно отслеживать индекс каждого из этих символов. Хотя вполне возможно, что я не понимаю, как это сделать правильно.   -  person Petersnow2    schedule 02.10.2018
comment
Обновление: я немного оптимизировал его. Хотя я на 90% уверен, что существует древовидная структура данных, которую я мог бы использовать, чтобы сделать это еще более производительным, я собираюсь поговорить с некоторыми из моих профессоров колледжа, чтобы узнать, что они думают. Я сделал это примерно в 4 раза быстрее (независимо от размера данных), так что я думаю, что, по крайней мере, что-то получаю.   -  person Petersnow2    schedule 02.10.2018
comment
@Kneesnap У вас есть набор тестов с этим классом? Тогда с ним проще возиться.   -  person Hiery Nomus    schedule 02.10.2018
comment
Да, я, вероятно, должен сделать один :P Поскольку это не профессиональный проект, я подумал, что могу быть ленивым. Хотя было бы неплохо начать прямо сейчас.   -  person Petersnow2    schedule 03.10.2018


Ответы (1)


Вам не хватает множества оптимизаций, особенно низкоуровневых.

Map<Byte, List<Integer>>

Это очень неэффективно.

На самом деле Map работает довольно быстро, но гораздо медленнее, чем массив. Вместо map.get(someByte), который выполняет автоупаковку и поиск карты (некоторые вычисления индекса и несколько обращений к массиву), вы можете сделать доступ к одному массиву, используя array[someByte & 0xFF], получая ускорение примерно на порядок.

Точно так же List<Integer> подразумевает автоупаковку, поскольку вы начинаете с ints. Автоупаковка обычно приемлема, но не тогда, когда она лежит в основе требовательного алгоритма. Вы можете написать собственный класс, ведущий себя как List<int>, или поискать его в Google (для этого есть несколько хороших библиотек).


if (!dictionary.containsKey(test))
    return -1; // No results found.

List<Integer> possibleResults = dictionary.get(test);

Это ненужный двойной поиск. Если вы не используете значения null, это можно записать как

List<Integer> possibleResults = dictionary.get(test);

if (possibleResults == null)
    return -1; // No results found.

Это в два раза быстрее, но, как я уже писал, здесь следует использовать массив.


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

person maaartinus    schedule 01.10.2018
comment
Да, к сожалению, я ищу более высокоуровневую вещь. Меня больше волнует временная сложность, чем подобные вещи. Но! Я внесу эти изменения. - person Petersnow2; 02.10.2018
comment
Предложения от @maaartinus наверняка улучшат ваше большое О. Любая ненужная операция в тесном цикле увеличивает временную сложность. Особенно такие вещи, как поиск по карте (logN) или просмотр списка (N) - person Hiery Nomus; 02.10.2018