/ Inżynier wydajności
← Wszystkie wpisy
lip 29, 2026 · 6 min czytania · Wydajność buildów

parallel-zip 1.4.0: 26× szybciej — i dokładnie ile to kosztuje

Nowy tryb domyślny pomija ponowną kompresję wszystkiego, co rozpoznaje jako już skompresowane, zamiast próbkować i zgadywać. To duża wygrana w szybkości — i realny koszt rozmiaru, którego nie zamierzam przed wami ukrywać.

parallel-zip 1.4.0 jest już dostępny. Krótka wersja: jest sporo szybszy niż 1.3.0, z powodu, który warto porządnie wyjaśnić — a ten powód ma realny koszt, o którym wolę powiedzieć wprost, zamiast chować go w changelogu.

Próbkowanie kontra czytanie etykiety

1.3.0 miał już obronę przed marnowaniem CPU na zawartość, która się nie skompresuje: sprawdź pierwsze 64 KiB dużego wpisu, i jeśli DEFLATE ledwo je skurczy, zapisz resztę bez zmian zamiast płacić za pełne przejście, które się nie zwróci. To statystyczny zakład — dobry, ale wciąż zakład. Próbkuje tylko początek wpisu, co bywa niereprezentatywne (META-INF/MANIFEST.MF jara siedzi tuż na początku i kompresuje się świetnie, nawet gdy pliki class za nim już nie), i włącza się dopiero dla wpisów wystarczająco dużych, żeby próbka miała sens.

1.4.0 dokłada coś bardziej bezpośredniego: sprawdza sygnaturę pliku wpisu — te same magiczne bajty, których używa twój system operacyjny, żeby zgadnąć typ pliku — względem listy formatów, które są już skompresowane. ZIP i wszystko na nim zbudowane (jar/war/ear/apk/whl/docx/xlsx…), gzip, bzip2, xz, 7-zip, zstd, rar, lz4, png, jpeg, gif, webp, mp4/mov, webm/mkv, ogg, woff/woff2. Trafienie od razu zapisuje wpis przez STORE, bez żadnej próby DEFLATE — taniej niż statystyczna próbka, i nie obchodzi go, jak duży jest wpis ani jak wygląda jego wysemplowany fragment.

To ten sam pomysł co verbatim copy w shaded-jar: przestań otwierać zaklejone pudło, żeby sprawdzić, czy warto je pakować od nowa. Tym razem pudło nie ma nawet central directory jako skrótu, na którym można się oprzeć — tylko swoje własne, pierwsze bajty, które okazują się wystarczające.

Liczby

Powtórzyłem cały zestaw benchmarków: te same 9 prawdziwych projektów open source i ich realnych zadań Zip z wydania 1.3.0, tym razem porównując faktyczny opublikowany jar 1.3.0 z nowym buildem 1.4.0, plus świeże przejście przez wszystkie 11 projektów z korpusu stałego (pełne szczegóły w dokumencie z benchmarkami pluginu).

Przyspieszenie parallel-zip 1.4.0 względem standardowego Gradle Zip w 9 prawdziwych zadaniach Zip: od 5.89× na SonarQube do 50.53× na JBake

Średnia geometryczna przyspieszenia względem standardowego Gradle Zip w tych dziewięciu zadaniach: ~26.5×, w górę z ~5.4× na 1.3.0. JBake i Micronaut Starter CLI są na szczycie wykresu z tego samego powodu — ich dystrybucje są gęsto upakowane małymi, już skompresowanymi jarami, dokładnie tym, na co celuje ta funkcja. SonarQube siedzi na dole (5.89×) nie dlatego, że funkcja tam nie pomaga, ale dlatego, że jego archiwum instalatora było już blisko tego pułapu na 1.3.0 — po prostu zostało mniej miejsca do odzyskania.

Część, której nie zamierzam ukrywać

Pominięcie przejścia DEFLATE dla wpisu „prawdopodobnie już skompresowanego“ oznacza pominięcie go nawet wtedy, gdy DEFLATE mógłby jeszcze uszczknąć procent czy dwa więcej, niż plugin teraz zostawia na stole. To nie błąd — to dokładnie ten kompromis, który jest zawierany — ale jest realny, i widać go w rozmiarze archiwum:

Wzrost rozmiaru archiwum parallel-zip 1.4.0 względem standardowego Gradle Zip w 9 prawdziwych zadaniach Zip: od 6.3% na SonarQube do 16.8% na Spring Boot CLI

6–17% większe niż standardowy Zip, w każdym pojedynczym projekcie z zestawu. 1.3.0, dla porównania, trzymał się w granicach ~0.3% od standardu — praktycznie za darmo. To zauważalnie inna oferta niż „szybciej za nic“, i wolę pokazać wam uczciwy wykres niż zestaw samych highlightów.

Furtka

Patrząc na ten drugi wykres, „zawsze włączone domyślnie“ przestało wydawać się słuszną decyzją dla każdego archiwum. Więc 1.4.0 dostaje właściwość zadania, żeby to wyłączyć:

tasks.register('dist', com.ljarocki.parallelzip.ParallelZip) {
    skipAlreadyCompressed = false   // domyślnie: true
}

false wraca do zawsze próbowanego DEFLATE — bliżej rozmiaru z 1.3.0, kosztem większości powyższego przyspieszenia. Statystyczny sniff z 1.3.0 zostaje aktywny niezależnie od tego ustawienia; ta flaga rządzi tylko nowym sprawdzaniem sygnatury. Domyślnie zostaje true, bo dla większości prawdziwych archiwów szybkość jest tego warta — ale „większość“ to nie „wszystkie“, i teraz nie musisz akceptować kompromisu, o który nie prosiłeś.

(Przy okazji doprowadzania tej flagi znalazłem i naprawiłem realną lukę: ścieżka kodu, która kompresuje kilka małych plików jednym natywnym wywołaniem — optymalizacja wsadowa dla archiwów z mnóstwem drobnych wpisów — w ogóle nie sprawdzała sygnatury, robiła to tylko ścieżka dla pojedynczych wpisów. Małe, już skompresowane pliki, które trafiły razem do jednej partii, całkowicie pomijały tę optymalizację. Teraz jest naprawione, a nowa flaga rządzi tą ścieżką tak samo konsekwentnie.)

Dwie mniejsze poprawki

Nie wszystko w tym wydaniu dotyczy kompromisu kompresji:

  • Naprawiony błąd blokady pliku, tylko na Windowsie. Duże wpisy mogą być kompresowane przez widok pliku źródłowego zmapowany w pamięci (mmap), dla szybkości. Na Windowsie niezwolnione mapowanie trzyma swój plik źródłowy zablokowany przed usunięciem albo nadpisaniem, dopóki garbage collector się nim nie zajmie — a to nie jest ograniczone w czasie, i czasem blokada wciąż trzymała, gdy kolejne zadanie w tym samym demonie Gradle próbowało dotknąć tego samego pliku. Mapowanie jest teraz jawnie zwalniane zaraz po powrocie z natywnego wywołania, zamiast czekać na GC.
  • Zabezpieczenie przed OutOfMemoryError na małych stertach demona Gradle. Plugin już wcześniej ograniczał, ile bajtów buforowanej pracy kompresji trzyma naraz w pamięci, skalując to do ułamka sterty demona — ale ten budżet nie brał pod uwagę, jak kilka dużych buforów w locie potrafi pofragmentować małą stertę, zanim nominalny budżet w ogóle zostanie osiągnięty. Rozmiar bufora na ścieżce mmap teraz też skaluje się w dół razem z rozmiarem sterty, a jednorazowe ostrzeżenie pojawia się, gdy sterta demona wygląda na zbyt małą do budowanego archiwum.

Jak to zdobyć

plugins {
    id 'com.ljarocki.parallel-zip' version '1.4.0'
}

Pełne notatki wydania są na GitHubie; kompletne zestawienie benchmarków — wszystkie 9 projektów in-build, wszystkie 11 projektów z korpusu stałego, cztery kodeki — jest w docs/BENCHMARKS.md.

ŁJ
Łukasz Jarocki
Inżynier wydajności