Il 31% più piccolo e fino all'83% più veloce di LZ4
Ingegneria Prestazioni 11 ottobre 2026

Il 31% più piccolo e fino all'83% più veloce di LZ4

Un decoder che legge i blocchi di liblz4 più velocemente di liblz4 stesso su 20 dei 27 profili misurati, più un formato nativo opzionale più piccolo del 31%. L'adozione parte da una sola chiamata di funzione modificata, misurata su quattro processori con tutte le sconfitte pubblicate.

C
Christian Schnapka
Macstab Engineering
6 min di lettura
Anche su Medium Leggi su Medium

Codice sorgente su GitHub

Il primo passo non costa nulla

lzvolt legge i blocchi propri di LZ4: non un formato simile, ma esattamente i byte scritti da liblz4, secondo la specifica dei blocchi LZ4. Su 20 dei 27 profili misurati li legge più velocemente di liblz4 stesso.

L'adozione più economica è quindi: tenere liblz4 come compressore, cambiare il decoder, fermarsi. LZ4_decompress_safe diventa lzvolt_decompress_lz4, con gli stessi argomenti. I dati memorizzati non si spostano, il percorso di scrittura resta intatto e non c'è nulla da annullare, perché nulla è stato convertito. Per molti sistemi è il posto giusto in cui restare: si decodifica mille volte per ogni scrittura.

La ricodifica nel formato nativo è un secondo passo, facoltativo. È lì che stanno il 31% e il fattore due. Ed è reversibile: decodificare con il nuovo, comprimere con il vecchio. Il percorso di ritorno è limitato dall'encoder di liblz4 — 2,51 GiB/s contro 11,86 — quindi un terabyte di dati logici torna indietro in circa sette minuti-core.

Due regole se convertite. Prima i lettori: un decoder LZ4 rifiuta correttamente uno stream nativo, quindi una sola replica non riavviata rovina la giornata. E finché i due formati convivono, tenete un flag per ogni valore: volutamente non esiste un magic number, quindi un blob non può essere riconosciuto dal contenuto.

Diagramma: byte liblz4 convertiti nel formato nativo e di nuovo in byte liblz4, con due chiamate in ciascuna direzione
La porta si apre in entrambe le direzioni: nulla viene ricodificato se non lo volete, e tornare indietro significa decodificare più LZ4_compress.

Cosa ottenete: due guadagni che si moltiplicano

Capacità e throughput si moltiplicano invece di sommarsi: il 44,7% di dati attivi in più nella stessa RAM e il 36% più veloce per core. Su record da 64 KiB il prodotto è +101,5% sull'M2 Max, +106,6% su Neoverse V2, +96,3% su Xeon e +79,1% su EPYC. In termini di acquisti, una cache che mantiene un dato working set a un dato ritmo di lettura ha bisogno di circa la metà delle macchine.

Non si limita nemmeno al profilo più comprimibile. Su dati variati da 64 KiB la capacità è dell'1,4% peggiore rispetto a LZ4, e la combinazione resta comunque tra +62% e +89%, sostenuta dalla sola decodifica.

Inoltre le mie dimensioni includono un header di lunghezza, mentre i blocchi nudi di LZ4 non portano alcuna lunghezza. Un valore LZ4 autonomo significa il formato frame, che costa 23 byte fissi. L'unica cella di record che perdo — 512 byte, dell'1,6% — la vinco del 9,3% contro un valore LZ4 autosufficiente.

Grafico: +44,7% di capacità per +36% di throughput, e il guadagno combinato su M2 Max, Neoverse V2, Xeon 8481C ed EPYC 9B14
Capacità e throughput si sommano in modo composto. Su record da 64 KiB il prodotto si avvicina al fattore 2 su tutte e quattro le macchine.

Come funziona: tre decisioni sui bit

Nessun nuovo algoritmo. LZ77 con le stesse due primitive che usano tutti, sequenze di letterali e riferimenti all'indietro, più tre decisioni su come spendere i bit che le implementazioni consuete non prendono.

Due forme di token. Il token di LZ4 è un byte: quattro bit di lunghezza dei letterali, quattro di lunghezza del match. Su record variati da 4 KiB, il 49% delle lunghezze di match cade sotto il limite dei quattro bit e il 47% sopra, a cavallo, così il branch decisivo cambia continuamente direzione e il predittore è inutile. Il formato prevede quindi due layout, e uno stream può passare dall'uno all'altro esattamente una volta. Il layout largo scambia due bit di letterali con un quinto bit di match, spostando l'escape da 19 a 35 byte, più un bit per un flag di ripetizione: stesso offset dell'ultima volta.

La lunghezza vive nello stream. Un header da uno a quattro byte, costruito come gli interi a lunghezza variabile di QUIC, porta la lunghezza decodificata, e la codifica più corta è obbligatoria. Il decoder alloca una sola volta, esattamente, e tratta un troncamento come un errore invece che come una risposta breve plausibile.

Il compressore rifiuta. Una sonda rapida scarta i dati non ripetitivi senza eseguire alcuna ricerca di match, e una soglia di un ottavo decide il resto. Al di sotto, il valore viene memorizzato grezzo e compress restituisce false, perché leggere un valore compresso costa da quattro a cinque volte più che leggerne uno memorizzato. liblz4 non ha questo passaggio.

text
saved >= 8 && saved * 8 >= original
Layout dei bit della suddivisione pari, identica al token di LZ4, e della suddivisione larga con bit di ripetizione
Un byte, diviso in due modi. Uno stream parte pari e può passare a largo esattamente una volta, in una posizione registrata.

Perché è veloce

Un decoder è un ciclo sul proprio input. Su record da 64 KiB il mio percorre 5524 byte dove liblz4 ne percorre 7992, per gli stessi 65536 in uscita. Date a un ciclo il 31% di input in meno e avrete fatto il 31% in meno di quasi tutto. Il guadagno di dimensione e quello di velocità sono un unico risultato misurato due volte.

Altri tre meccanismi. Un terzo dei blocchi non carica mai un offset: 493 su 1567 portano il flag di ripetizione. Un unico load a 32 bit fornisce token, offset e lunghezza del match quando la sequenza di letterali è vuota: il 56% dei blocchi su 4 KiB variati, l'88% su record da 64 KiB. E sopra una certa soglia memcpy batte un ciclo scritto a mano, perché quello di glibc è ottimizzato da persone che non fanno altro.

Il kernel è limitato dalla latenza, non dall'issue: il ciclo di copia gira a un IPC di 5,37 contro un'ampiezza di issue di sei. Eliminare un'istruzione non porta nulla; eliminare un load dipendente dalla catena critica fa guadagnare tre cicli.

Una riserva onesta: non ho fatto un'ablazione di questi meccanismi. I singoli numeri sono misurati; la loro classifica è un'inferenza, non una scomposizione misurata.

I numeri, e cosa non affermo

Tre macchine, tre esecuzioni ciascuna, mediana di tre, interlacciate con liblz4 in un unico processo: stessi byte, stessa macchina, stesso minuto. Decodifica del formato nativo: diciotto celle, diciotto vittorie, da +14% a +83%. Nella lettura dei blocchi propri di LZ4 vince 20 su 27; le sette sconfitte si concentrano su valori piccoli e incomprimibili su x86, dove cedo il 12–15%.

Per le dimensioni, quattro profili su sei sono più piccoli che con LZ4 e due più grandi: 64 KiB variati dell'1,4%, record da 512 byte dell'1,6%. liblz4 è il pacchetto standard della distribuzione, mentre dalla mia parte c'è assembly scritto a mano e selezionato per linea di processore: onesto per il deployment, ma non un confronto a parità di sforzo. Governor della CPU, turbo e pinning dei core non sono stati controllati, quindi i margini a una cifra vanno trattati con sospetto.

Il codec include cinque corpi di decoder assemblati separatamente, perché modifiche identiche hanno dato segni opposti su processori diversi: allineare la sorgente prima della copia vale +49% su Neoverse V2 e −7,1% sull'M2 Max, entrambi aarch64. Un numero di benchmark senza il nome della macchina è rumore.

Ho misurato contro liblz4 e lz4_flex, non contro Selkie di Oodle, lzturbo, Snappy, LZO o zstd ai livelli negativi. Quindi, con precisione: sul formato che l'industria ha standardizzato, su quattro processori, LZ4 non è più la cosa più veloce che lo legge. È tutto ciò che i numeri sostengono.

Grafico a barre del vantaggio di throughput in decodifica su liblz4 per sei profili di dati su Xeon 8481C, EPYC 9B14 e Neoverse V2
Decodifica del formato nativo contro liblz4: 18 celle su 18 in vantaggio, da +14% a +83%.

Cosa non hanno trovato i test verdi

Circa venti modifiche deliberate hanno dato un risultato nullo o negativo. Tre hanno sostenuto il progetto, e tutte e tre hanno eliminato lavoro invece di rendere più economico il lavoro esistente.

L'harness di misura ha mentito quattro volte, l'ultima a causa dell'aliasing a 4 KiB: stesso binario, 32% di differenza, perché malloc posizionava i buffer in modo diverso in ogni processo. Ora usa un'unica arena a offset fissi, prende la mediana di cinque rapporti e stampa una soglia di risoluzione del 6% per cella.

Tre bug sono sopravvissuti a suite di test verdi, tra cui un decoder che eseguiva 512 byte della propria tabella di lookup come codice macchina e una violazione di provenance trovata solo da Miri. Ogni test partiva da uno stream scritto dal compressore; un attaccante non ha questo vincolo. Ora le modifiche all'assembly vengono accettate solo dopo che una corruzione deliberata ha fatto diventare rossa la suite, e quattro target cargo-fuzz hanno eseguito 115.090.390 esecuzioni sotto AddressSanitizer senza crash, il che non è una prova di assenza.

La lezione di fondo: ogni problema emerso alla fine era un'affermazione sul codice che nessuno aveva eseguito. Eseguite la frase prima di scriverla.

Key Takeaways

lzvolt = "1.0" richiede rustc 1.85 o successivo, e la release include librerie precompilate .so, .dylib e .dll con header C per Linux, macOS e Windows su amd64 e arm64. Windows esegue il Rust portabile invece dell'assembly ed è più lento; l'API indica quale percorso state usando.

Iniziate con make quick: circa sette secondi, e stampa il nome del vostro processore e un verdetto. Poi puntate lzvolt_decompress_lz4 su blocchi che avete già, lasciate comprimere liblz4 e confrontate. Nulla ricodificato, nulla migrato: un pomeriggio per scoprire se ho torto. Il formato ha una specifica normativa e 16 vettori di test per decoder, e il registro delle misurazioni nel repository contiene ogni numero e tutte le venti confutazioni.

#compression #lz4 #rust #linux #performance
C

Christian Schnapka

Macstab Engineering

Christian Schnapka costruisce gli strumenti di ingegneria aperti dietro Macstab. Pubblica le versioni estese di queste indagini su Medium, con ogni misurazione, ogni riserva e ogni sconfitta incluse.