From 81ee260c4edd2047d80f55e10005f099340880b4 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Thu, 22 Oct 2026 03:45:59 -0700 Subject: [PATCH] level 2 : check ip+1 after any short match LZ4MID_compress() checked ip+1 for a longer match only after a match found by the 4-byte table within the prefix, never after a match found by the 8-byte table. The check now follows any match shorter than 8 bytes, also within an external dictionary, and skips the candidate ip itself: inside runs, ip+1 would find ip at offset 1, and periodic data would lock onto one literal and one offset 1 per run (4 KB to ~1.6 KB). The previous check is removed. Compressed size : linux-6.12 tar -1.3%, Silesia -0.6%, enwik9 -0.5% (4 MB blocks), -0.25% (4 KB blocks), -0.9% (64 KB linked blocks), -0.2% (4 KB pages with a 64 KB dictionary). Speed, AMD EPYC Genoa (Zen 4), x86-64, gcc 11 / gcc 15 / clang 22 : -0.5 to -1.3% (256 KB blocks), -1.3 to -2% (4 KB blocks), -1.8 to -2.5% (64 KB linked blocks), -0.2 to -1.2% (4 KB pages with a dictionary). x86 (gcc 12 -m32) : +1.4% (4 KB pages with a dictionary) to -4.9% (64 KB linked blocks). --- NEWS | 2 +- lib/lz4hc.c | 33 ++++++++++++++------------------- tests/fuzzer.c | 11 +++++++++++ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/NEWS b/NEWS index 2e4eee00..0dbb26dd 100644 --- a/NEWS +++ b/NEWS @@ -26,7 +26,7 @@ perf : `LZ4_FAST_DEC_LOOP` enabled on aarch64 with clang, including Android (up perf : `LZ4_FAST_DEC_LOOP` enabled on riscv64 when unaligned accesses are fast (`-mno-strict-align`, or a `-mcpu` with fast unaligned accesses) perf : faster compression with reused contexts, for blocks >= 1 KB : linked frames (up to +69%), `LZ4_resetStream_fast()` (up to +94%) perf : faster compression of 2-4 KB blocks with `LZ4_attach_dictionary()` (+3% to +30% on Zen 4) -perf : level 2 compresses slightly better (~0.1-0.5%) and faster (~+1-2%) +perf : level 2 compresses better (~0.2-1.5%), at similar speed (-3% to +1% on Zen 4) perf : faster levels 11 (~+25%) and 12 (~+10%) perf : `--favor-decSpeed` : faster decompression (~+2-4% at level 12), with fewer sequences api : lz4frame compression accepts any `dstCapacity`, and fails only if the output doesn't fit diff --git a/lib/lz4hc.c b/lib/lz4hc.c index 6fd6e7e4..b7ebc2fd 100644 --- a/lib/lz4hc.c +++ b/lib/lz4hc.c @@ -723,35 +723,30 @@ static int LZ4MID_compress ( LZ4MID_addPosition(hash8Table, h8, ipIndex); matchLength = LZ4MID_countMatch(ip, ipIndex, pos8, matchlimit, prefixPtr, prefixIdx, dictStart, dictIdx); matchDistance = ipIndex - pos8; - if (matchLength >= MINMATCH) goto _lz4mid_encode_sequence; } /* search short match */ - { U32 const h4 = LZ4MID_hash4Ptr(ip); + if (matchLength < MINMATCH) { + U32 const h4 = LZ4MID_hash4Ptr(ip); U32 const pos4 = hash4Table[h4]; assert(h4 < LZ4MID_HASHTABLESIZE); assert(pos4 < ipIndex); LZ4MID_addPosition(hash4Table, h4, ipIndex); matchLength = LZ4MID_countMatch(ip, ipIndex, pos4, matchlimit, prefixPtr, prefixIdx, dictStart, dictIdx); matchDistance = ipIndex - pos4; - if ((matchLength >= MINMATCH) && (pos4 >= prefixIdx)) { - /* short match found in prefix, let's just check ip+1 for longer */ + } + if (matchLength >= MINMATCH) { + if ((matchLength < 8) && likely(ip < mflimit)) { + /* short match : check ip+1 for longer, except ip itself */ U32 const h8 = LZ4MID_hash8Ptr(ip+1); U32 const pos8 = hash8Table[h8]; - U32 const m2Distance = ipIndex + 1 - pos8; - if ( m2Distance <= LZ4_DISTANCE_MAX - && pos8 >= prefixIdx /* only search within prefix */ - && likely(ip < mflimit) - ) { - const BYTE* const m2Ptr = prefixPtr + (pos8 - prefixIdx); - unsigned ml2 = LZ4_count(ip+1, m2Ptr, matchlimit); - if (ml2 > matchLength) { - LZ4MID_addPosition(hash8Table, h8, ipIndex+1); - ip++; - matchLength = ml2; - matchDistance = m2Distance; - } } - } - if (matchLength >= MINMATCH) goto _lz4mid_encode_sequence; + unsigned const ml2 = LZ4MID_countMatch(ip+1, ipIndex+1, pos8, matchlimit, prefixPtr, prefixIdx, dictStart, dictIdx); + if ((ml2 > matchLength) && (pos8 != ipIndex)) { + LZ4MID_addPosition(hash8Table, h8, ipIndex+1); + ip++; + matchLength = ml2; + matchDistance = ipIndex + 1 - pos8; + } } + goto _lz4mid_encode_sequence; } /* no match found in prefix nor extDict : search into the attached dictionary */ if ( (dict == usingDictCtxHc) diff --git a/tests/fuzzer.c b/tests/fuzzer.c index 1daedd3e..197ef587 100644 --- a/tests/fuzzer.c +++ b/tests/fuzzer.c @@ -1734,6 +1734,17 @@ static void FUZ_unitTests(int compressionLevel) } DISPLAYLEVEL(3, "OK \n"); + DISPLAYLEVEL(3, "level 2 : the match tested at ip+1 must not be ip itself, inside runs : "); + { char runs[2 + 4096]; /* 0xFF 0x00, then runs of 10 x 0xFF and 10 x 0x00 */ + int i; + runs[0] = (char)0xFF; runs[1] = 0; + for (i = 0; i < 4096; i++) runs[2 + i] = (i % 20) < 10 ? (char)0xFF : 0; + result = LZ4_compress_HC(runs, testCompressed, (int)sizeof(runs), (int)testCompressedSize, 2); + /* ~1.6 KB (one literal and one offset 1 per run), instead of < 50 bytes */ + FUZ_CHECKTEST(result <= 0 || result > 100, "%i bytes", result); + } + DISPLAYLEVEL(3, "OK \n"); + /* saveDictHC test #926 */ DISPLAYLEVEL(3, "saveDictHC test #926 : "); { LZ4_streamHC_t* const ctx = LZ4_initStreamHC(&sHC, sizeof(sHC));