Алгоритмы сжатия данных с помощью Java
https://t.me/android_itsСжатие данных – очень распространенная тема. В интернете мы можем найти множество материалов о ней. Существуют разные тесты для всех видов алгоритмов сжатия. Тесты производительности для Java существуют, но, все они немного устарели, поскольку были написаны давным-давно.
Эффективность сжатия (как производительности, так и размера) зависит от фактических данных. Поэтому для меня имело смысл повторить тесты, чтобы определить, какой алгоритм (если таковой имеется) подойдёт больше всего. Также хотелось просто побольше узнать о сжатии данных в Java.
Какие данные мы сравниваем?
Мы собираемся провести сравнение двух наборов данных: нашей внутренней структуру данных разного размера и набора искусственно сгенерированных случайным образом данных с различными степенями сжатия и размерами.
Внутренняя структура данных – это своего рода trie с URL-адресами, UUID и другой двоичной информацией (закодированной как сообщения protobuf).
Случайный датасеты – это просто рандомные символы из разных наборов. Вот функция для генерации данных:
private static java.util.Random rnd = ...; private static byte[] generateImpl( byte[] alphabet, int length) { byte[] result = new byte[length]; for (int i = 0; i < result.length; i++) { result[i] = alphabet[rnd.nextInt(alphabet.length)]; } return result; }
Для разных текстов я рассчитал коэффициенты сжатия и вызвал разные наборы в соответствии с их коэффициентами:
low (1.3) -> "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" medium (2.1) -> "0123456789" high (3.4) -> "0123" extra high (6.2) -> "01"
Дисклеймер: случайный датасет не является полностью рандомным, как вы можете видеть, для целей сравнения. Это просто нестатический набор данных с более или менее стабильной степенью сжатия.
Алгоритмы сжатия
Я выбрал несколько популярных алгоритмов сжатия:
- gzip через Apache Commons Compress
- deflate также через Commons Compress
- MySQL compression: в основном, он использует алгоритм deflate, а также префиксы с 4 байтами исходной длины данных (что помогает выделить буфер для распаковки). Через java.util.zip.Deflater.
- zstd: алгоритм сжатия Facebook, который предположительно быстрее и имеет лучшую степень сжатия, чем deflate.
- snappy: алгоритм сжатия Google, который был разработан таким образом, чтобы быть очень быстрым при разумных коэффициентах сжатия. Через snappy-java.
- brotli: еще один алгоритм сжатия от Google, который нацелен на лучшую степень сжатия и сопоставимую производительность с deflate. Для brotli мы будем использовать 3 уровня сжатия: 0, 6 и 11 (по умолчанию, максимальный). Используя Brotli4j.
- lz4: алгоритм сжатия, который направлен на достижение наилучшей производительности декодирования. Для lz4 мы будем использовать 3 уровня сжатия: быстрый, 9 (по умолчанию), 17 (максимальный). Через lz4-java.
Степени сжатия
Для реального набора данных лучшую степень сжатия предоставляет brotli, худшую snappy и lz4 fast:
Для случайного датасета лучшую степень сжатия предоставляет brotli, а худшую, неизменно, lz4 fast.
Электронная таблица со всеми коэффициентами сжатия доступна здесь.
Тесты производительности
Тесты были выполнены с использованием JMH для 3 JDK: openjdk-8, openjdk-11 и openjdk-17.
Декодирование реальных данных
Во-первых, давайте посмотрим на производительность декодирования для реальных данных (длина данных составляет от 600 КБ до 4 МБ):
Другим представлением для этой диаграммы была бы пропускная способность в байтах в секунду:
Из этих графиков видно, что lz4 (все 3 уровня) значительно превосходит все остальные алгоритмы. Следующими после lz4 fast являются zstd и snappy.
Кодирование реальных данных
Далее давайте проверим производительность кодирования для реальных данных (длина данных составляет от 600 КБ до 4 МБ):
Ух ты! Brotli с 11-м уровнем потребовалось 13 секунд, чтобы закодировать 4 МБ данных… Сначала я не поверил в это, подумал, что, должно быть, неправильно пользуюсь библиотекой. Но потом я запустил оригинальный родной двоичный файл:
$ time brotli ~/1/bins/total_4072805_with_34010_items.bin real 0m12.945s user 0m12.878s sys 0m0.064s
Действительно, 11 уровень сжатия чрезвычайно медленный. Давайте исключим его, чтобы увидеть нормальную работу других алгоритмов:
И соответствующий график пропускной способности (байты в секунду):
Здесь 4 лидера: lz4 fast (500+Мбит/с), brotli_0 (450+Мбит/с), snappy (400+Мбит/с) и zstd (250+Мбит/с). Худший – lz4_high17 с 13-30 Мбит /с.
Сравнение производительности в разных JDK
Сравнивая производительность в разных JDK, мы можем увидеть, что производительность более или менее одинакова, поскольку большинство библиотек используют JNI под капотом:
То же самое и для кодирования:
Сравнение тестов случайного датасета
Несмотря на то, что коэффициенты сжатия в реальном наборе данных разные, в основном они равны 2 и выше, плюс не все данные случайны. Также интересно проверить, как алгоритмы ведут себя при более разных степенях сжатия и большем количестве случайных данных. Итак, давайте проверим производительность рандомных данных при различных степенях сжатия.
lz4 fast vs snappy
Глядя на графики пропускной способности, кажется, что lz4 fast compressor – очень достойная альтернатива snappy, когда степень сжатия не очень важна. Декодирование происходит быстрее:
А теперь кодирование:
Однако, этот тест не учитывает ограничение ввода-вывода системы: пропускная способность сети / диска не бесконечна, поэтому сжатие не будет работать с максимальной производительностью. Я попытался эмулировать ограничение ввода-вывода, но, похоже, у меня это плохо получилось. Поэтому я собираюсь скоро запустить еще один тест…
gzip vs deflate
Gzip и deflate имеют разницу в размерах в 12 байт. Кроме того, gzip использует deflate для сжатия данных. Но между ними есть разница в производительности!
Это очень странно, потому что под капотом как deflate, так и gzip используют классы Inflater/ Deflater
из JDK.
deflate vs MySQL Compress
Размер разницы (дополнительные 4 байта) позволяет точно распределять буфер при декодировании, что может сэкономить несколько выделений и копирование памяти. И это то, что мы действительно видим на графике:
Здесь оптимизация действительно очень помогает.
brotli vs gzip
Предполагается, что Brotli является более эффективной заменой deflate / gzip. С точки зрения производительности, Brotli с уровнем сжатия 6, как правило, быстрее, чем gzip / deflate. После определённого размера (в этом тесте где-то между 20K и 30K) Brotli работает значительно лучше:
То же самое касается и кодирования, но там порог еще меньше (около 3K-4K):
Другие интересные детали
Brotli
Существует 3 библиотеки для Java:
- org.brotli:dec: старая библиотека от Google, последнее обновление было в 2017 году.
- com.nixxcode.jvmbrotli:jvmbrotli: тоже старая, обновлена в 2019 году.
- com.aayushatharva.brotli4j:brotli4j: та, которую я использовал при сравнении.
Библиотека dec
допускает только декомпрессию. Я решил провести сравнительный анализ, и результаты очень печальные:
Action (algorithm) (ratio) (length) Score Units decode brotli_6 medium 102400 813.029 us/op decode dec medium 102400 2220.280 us/op decode jvmbrotli medium 102400 2644.517 us/op
lz4
lz4 поддерживается в Commons Compress. Краткий тест показал, что он более чем на 1 порядок медленнее, чем org.lz4:lz4-java.
Action (algorithm) (length) Score Units decode lz4_block 62830 726.496 us/op decode lz4_framed 62830 891.120 us/op decode lz4_fast 62830 30.119 us/op decode lz4_high17 62830 22.561 us/op decode lz4_high9 62830 22.075 us/op
Заключение
Несмотря на то, что результаты теста выглядят многообещающими, я не до конца уверен, что увижу такой же уровень производительности в реальном приложении: я кратко упомянул, что ограничение ввода-вывода сети / диска тяжело эмулировать, поэтому трудно предсказать, как оно будет себя вести.
В любом случае,
- lz4 выглядит действительно многообещающе. Я с нетерпением жду возможности сделать с его помощью более полный тест производительности!
- Brotli удивил тем, что по умолчанию он использует максимальное сжатие, которое происходит чрезвычайно медленно. Ещё больше меня удивило то, что скорость декодирования сильно сжатых данных ниже, что делает средний уровень сжатия гораздо лучшим выбором.
- Zstd находится где-то посередине с точки зрения производительности и степени сжатия.
Поиграйте с графиками здесь. Исходный код находится на GitHub.
https://t.me/javatg – изучение java в нашем телеграм канале.