Архиватор, который разбирает формат, а не подбирает кодек

Гонка кодеков почти закончена: очередной процент у LZMA, BWT и контекстных моделей стоит кратного времени. Запас остался в другом — понимать, что за данные лежат внутри. Nova Prism разбирает JPEG, PDF, zip, MP3 и WAV на части, сжимает их заново и собирает обратно бит в бит.

На уже сжатых данных мы впереди всех, включая CM, и быстрее их. На обычных — впереди LZ и на несколько процентов позади CM.

1. Одной фразой

▲
Данные, которые можно разобрать по формату — zip, PNG, JPEG, PDF, apk, WAV: первые среди всех, включая CM, и быстрее их.
●
Обычные данные — впереди всего семейства LZ: 7-Zip, xz, brotli.
▼
Текст и смешанные корпуса — позади CM (kanzi, zpaqfranz) на единицы процентов.

как мы сюда пришли — четыре снимка

0.9.7 Linux, контейнеры без сигнатуры и три раунда аудита Потоки zlib ищутся по всему файлу; окно наконец спрашивает про перезапись packfile −13,7%

Сканер deflate смотрел только на начало файла, поэтому контейнер, который не объявляет себя с первого байта — git packfile, полезная нагрузка установщика, закрытый формат с потоками zlib внутри, — считался несжимаемым. Теперь кандидаты ищутся по всему файлу: три packfile этого репозитория −13,7%, на 11,8% меньше 7-Zip, а все прочие корпуса побайтово те же.

Появились пакеты .deb, .rpm, AppImage и статический np-cli (glibc 2.34+), а окно само сообщает о новой версии — ссылкой на файл именно для вашей системы.

Перед 1.0 код прошёл три раунда аудита, где каждую находку пытался опровергнуть отдельный проверяющий. Закрыты: испорченный блок, который 0.9.6 в редком случае записывал под верной контрольной суммой (архивы 0.9.6 стоит проверить np-cli test), запись за пределами буфера в декодере bsc на подделанном архиве, обрезка последнего поколения при гниении футера и манифест, способный запросить 8 ГБ памяти. Третий раунд искал не поломки, а МОЛЧАНИЕ, и нашёл его четыре раза: окно называло успехом упаковку, из которой часть файлов выпала, открывало повреждённый архив без единого слова, считало удачной распаковку, не записавшую ни одного файла, и теряло пути с именем не в UTF-8.

Последним закрылся не изъян кодека, а единственный ответ на вопрос, которого окно не задавало: распаковка всегда шла в режиме «пропустить существующие», поэтому восстановление поверх живого дерева не записывало ничего. Теперь окно спрашивает — и только когда есть о чём: если ни одна цель не занята, диалога нет и путь побайтово прежний. «Заменить» — первый случай, когда окно просит перезапись, и он вскрыл дыру: сторонние читатели zip и 7z не проверяли, не является ли цель самим открытым архивом. report.zip с записью report.zip внутри, распакованный на месте, стирал сам себя и отчитывался об успехе. Проверка добавлена в оба читателя.

0.9.6 Сканер не видел половину пакетов Windows Классический хвост zip врёт, а правду хранит запись ZIP64 — её не читали app.appx −38,3%

Оглавление zip хранит счётчик файлов 16 битами, а смещение оглавления — 32. Когда их не хватает, туда пишут заглушку, а правду кладут в отдельную запись ZIP64. Заглушка принималась за настоящее смещение, сканер находил ноль потоков, и файл ложился в архив как есть — без единого сообщения.

Дело оказалось не в архивах больше 4 ГБ: пакеты приложений Windows (.msix, .appx) пишут ZIP64 всегда, независимо от размера, и ни один такой пакет на этой машине не пережимался ни разу — самый мелкий найден на 344 КБ. Теперь оба сканера читают запись по её собственному признаку: app.appx −38,3%, Microsoft.SecHealthUI.appx −30,3%. Кодек и турнир этот снимок не тронул.

0.9.5 Адрес, который не выносил в отдельный поток никто Фильтр 42: адресация относительно счётчика команд в x86-64 .NET −9,8%

Все преобразователи машинного кода, какие есть, трогают ровно три команды: E8, E9 и 0F 8x, то есть относительный адрес перехода. А на 64-битном x86 есть второе такое же повсеместное 32-битное смещение — адресация относительно счётчика команд, через которую идёт каждое обращение к глобальной переменной и каждый вызов через таблицу импорта. Перепись 10,4 ГБ установленных программ объясняет, почему это стало важно только сейчас: 96,9% байт исполняемых файлов — 64-битные. Фильтр 42 забирает и эти адреса: .NET −9,8%, Firefox −6,2%, Wireshark −3,3%, Go −2,3%.

Итог двух версий: девять настоящих установок (Firefox, .NET, Wireshark, Go, Python, две на Electron, 2,74 ГБ) шли на 5,91% хуже 7z -mx9, потом на 1,77%, а теперь на 0,23% лучше. Одним архивом на всё дерево — на 4,5% меньше и на 13% быстрее, чем в 0.9.3.

Цена — совместимость в одну сторону: архив 0.9.5 с исполняемыми файлами старая версия не откроет и скажет об этом прямо, а 0.9.5 читает всё, что было написано раньше. Поднять предел 64 МиБ значит менять формат, и цена измерена: на обычных данных бо́льшая единица уже вредит — Silesia +1,10% при 48 МиБ.

0.9.4 Проигрыш оказался не в кодеке, а в дальности Вопрос «какой ТИП файлов проигрывает» вместо «какая установка» блок 64 МиБ, пары от 96 КБ

Ответ оказался неожиданно узким. У .exe отставание было 15,1% — и по одному файлу мы на 0,10% МЕНЬШЕ 7-Zip: весь проигрыш давали девять программ инструментария Go, в каждую из которых влинкована одна и та же среда исполнения. Один и тот же код лежал в наборе девять раз, а увидеть это можно, только если он попадает в одно окно. У .pak (интерфейсные строки Chromium) отставание было 29,6%, и там весь проигрыш — одна пара почти одинаковых наборов языковых файлов, лежавших в 56 МБ друг от друга. Ни то, ни другое не про кодек.

Отсюда план: блок с машинным кодом вырос до 64 МиБ, пары одноимённых файлов ищутся от 96 КБ вместо мегабайта. После этого на переписи по типам проигрывали только .exe и .dll — их забрала 0.9.5.

2. Где мы выигрываем, одной картинкой

По каждому корпусу — насколько наш архив меньше, чем у лучшего из всех остальных, а не просто чем у 7-Zip. Вправо от середины — мы впереди.

Верхние четыре строки — данные, которые принято считать законченными: остальным применить к ним нечего, а нам есть что. Пятая показывает границу: FLAC не берёт никто, включая нас. Нижние четыре — прямая гонка кодеков; отставание на дереве исходников и установленной программе — плата за возможность править архив по одному файлу, которой у сплошного потока 7-Zip нет.

3. Там, где мы первые

Это не «лучше настроенный LZMA». Это данные, которые остальные считают законченными и складывают как есть: Nova Prism разворачивает сжатие и делает его заново — и обходит здесь даже CM-класс, будучи при этом быстрее.

0.9.6: пакеты приложений Windows

.msix и .appx — тоже zip, но настоящие числа оглавления лежат у них в записи ZIP64. Сканер её не читал и складывал пакет как есть. Теперь читает, и на настоящих пакетах: app.appx (1 199 354 Б) — было 953 429, стало 588 435 Б, −38,3%; Microsoft.SecHealthUI.appx (10 262 607 Б) — было 7 816 188, стало 5 447 431 Б, −30,3%. Выигрыш есть не везде: у маленького Microsoft.Copilot.msix только −1,0% — внутри почти нечего пережимать. Каждый файл распакован и сверен с исходником побайтово.

почему это оказалось не про архивы больше 4 ГБ

Счётчик записей и смещение оглавления в классическом хвосте zip — 16 и 32 бита; когда числа туда не влезают, стандарт требует писать заглушку, а правду класть в ZIP64. Считалось, что это касается только больших архивов. Оказалось неверно вдвое: упаковщик Microsoft пишет ZIP64 всегда, и все пять системных пакетов, найденных на этой машине, устроены именно так — от 344 КБ.

0.9.7: контейнеры, которые о себе молчат

Множество форматов хранит внутри потоки zlib без всякой сигнатуры: git packfile, полезная нагрузка установщиков, ресурсы программ. Раньше сканер отправлял файл по первым байтам, и такой контейнер писался как есть — у 7-Zip и xz там тоже всего −2,2%. Теперь кандидаты ищутся по всему файлу: три packfile этого репозитория (4 355 190 Б) — 7z -mx9 4 260 799, nova было 4 355 856, стало 3 760 190 Б, −13,7%. Silesia, enwik8, дерево исходников, корпус deflate, установленные программы и наборы .pak — побайтово без изменений.

почему порог — покрытие, а не число найденных потоков

Случайный заголовок zlib встречается в двоичных данных примерно раз на 4 КиБ, поэтому голый поток распознаётся по заголовку и проверяется распаковкой, а в контейнерный фильтр файл попадает, только если потоки покрывают не меньше 95% просмотренного. Счёт вместо покрытия стоил дереву исходников +6,4%.

4. Звук: три задачи с разными ответами

Под словом «звук» скрываются три разные задачи, и ответы у них разные: несжатый WAV, уже сжатый FLAC и MP3 ведут себя совершенно по-разному.

Что оказалось важнее самого FLAC

По дороге нашлось отставание от 7-Zip на несжатом звуке — 28%. Кодек был ни при чём: фильтр разности у нас был, но не запускался — проверка «и так сжимается» пропускала его мимо, а WAV сжимается до 82%. Без проверки −22%, FLAC сверху — ещё −19%.

Сам FLAC пережимать смысла нет: лучший энкодер даёт −0,94%, а потолок для контекстной модели (paq8px) — 4,9% ценой в 700 раз медленнее допустимого.

5. Бюджет скорости решает, кто нам эталон

Правило не «не медленнее 7-Zip на максимуме», а быстрее него и меньше него одновременно. Это сразу отсекает верх таблицы рекордов — не по качеству, а по скорости: cmix распаковывает enwik9 около 75 часов, nncp сжимает его 2,8 суток на RTX 4090. Одно исключение оставлено намеренно: на уже сжатых файлах мы тратим в 11 раз больше 7-Zip и отдаём взамен на 30% меньший архив.

Nova Prism контекстное смешивание (CM) LZ / BWT / PPM вне бюджета скорости в кольце — измерено здесь

enwik9, 1 ГБ. Выше точка — меньше архив; правее — быстрее сжатие, шкала логарифмическая. Точки в кольце — nova, 7-Zip, xz, kanzi и zpaqfranz, снятые здесь в одном прогоне на восьми потоках: они сравнимы напрямую. Наша ушла вправо и вверх сразу — 61,5 секунды вместо 238,9 и 189,4 МБ вместо 195,0, а распаковка гигабайта заняла 12,6 секунды против 143,3 у kanzi -l9 и 565,4 у zpaqfranz -m5.

откуда взяты остальные точки

Из Large Text Compression Benchmark: один поток, другое железо, поэтому по горизонтали к ним стоит относиться как к порядку величины, а не к проценту. Наши точки по той же причине стоят правее, чем стояли бы в один поток.

6. Эталонные корпуса

enwik8, enwik9 и Silesia — потому что по ним публикуют результаты все, и наши размеры можно класть рядом с чужими без оговорок.

7. Турнир кодеков: кто на чём выигрывает

На максимальном уровне за каждую единицу сжатия соревнуются четыре кодека, и остаётся меньший результат. Универсального победителя нет — вот кто сколько единиц забрал.

Отсюда видно, почему вопрос «какой кодек лучше» не имеет ответа. На тексте всё забирает BWT. На исполняемых файлах и документах выигрывает LZMA2, но заметную долю берёт PPMd7 порядка 16. На дереве исходников решает не кодек, а устройство архива. Там, где формат уже энтропийно закодирован, турнир не проводится: моделировать нечего — библиотека FLAC собирается байт в байт та же за 43 секунды вместо 306.

почему кодек нельзя выбрать по классу данных

Проза и исходный код для программы одинаковы — и то и другое «текст», — а кодеки им нужны противоположные. Поэтому выбирает не правило, а отбор на пробе самой единицы: на 83 реальных единицах LZMA2 съедал 61% всего времени турнира, выигрывая 46% единиц; проба убирает большую часть этой работы, не меняя результат ни на байт.

8. bsc и kanzi: что взяли, что нет

По таблице LTCB решать было нельзя: там bsc считает весь гигабайт одним блоком на 5 ГБ памяти, а у нас единица сжатия 32 МиБ, и её размер — не настройка, а цена правки одного файла. Замер на нашей геометрии показал, что BWT выигрывает на тексте и проигрывает на дереве файлов — ровно тот случай, который решает турнир. libbsc внедрён кодеком 4 на максимальном и среднем уровне; почему не на быстром — ниже.

9. Масштабирование от 1 ядра к 8

Важнее не «во сколько раз быстрее», а меняется ли размер архива от числа потоков. Большинство архиваторов распараллеливаются тем, что режут данные на более мелкие независимые блоки: каждый добавленный поток там оплачен степенью сжатия. У nova размер единицы задан геометрией архива, поэтому вывод обязан быть байт в байт одинаковым на одном потоке и на восьми.

10. Что уже исследовано

18 отчётов в docs/research/ плюс собственные замеры. Строки отвергнуто ценны не меньше остальных: каждая — неделя, которую не потратят снова.

11. План

Порядок — по измеренной ценности, а не по интересности.

12. Что ещё можно изучить

13. Как это проверить

Все замеры сняты на одной машине — 8 логических ядер, Windows 11 — одной командой на корпус. Внутри таблицы инструменты сравнимы напрямую; между таблицами сравнимы размеры, но не время: корпуса снимались в разные прогоны. Версии эталонов: 7-Zip 26.02, xz 5.6, brotli 1.1, kanzi 2.5.1 (сборка на C++), zpaqfranz 64.8, paq8px v216.

enwik8, enwik9 и Silesia взяты по ссылкам выше в исходном виде. Корпус «уже сжатого» собирается скриптом из файлов с постоянными ссылками, рядом записаны их SHA-256 — он воспроизводится побайтово. Документы, фотографии и музыка пока собраны локально, помечены отдельно, и на публичные источники их ещё предстоит перевести.

о чужих цифрах на диаграмме

Точки не в кольце — из Large Text Compression Benchmark (Matt Mahoney) и Hutter Prize: другое железо и один поток, поэтому по горизонтали это порядок величины.

Снимок 0.9.7 — версия выпущена 20 сентября 2026 года девятью файлами: установщик и np-cli.exe для Windows, .deb, .rpm, AppImage и статический np-cli для Linux, install.sh, инструкция и SHA256SUMS. Это последняя бета перед 1.0 и первая во всей линейке 0.9, выложенная обычным релизом, а не пред-релизом. Изменились разделы 1, 3, 10 и 11; цифры турнира кодеков и эталонных корпусов те же, что в прошлом снимке. Архивы, созданные 0.9.6, стоит проверить командой np-cli test — почему, сказано в карточке 0.9.7 выше.