From b74437892c1df670a4f3d8864f3f888998b4ea6c Mon Sep 17 00:00:00 2001 From: Zhiyang Chen Date: Tue, 27 Oct 2026 00:48:48 +0100 Subject: [PATCH] [dfast] Use ZSTD_selectAddr() for prefix checks in dictMatchState ZSTD_compressBlock_doubleFast_dictMatchState_generic() tests matchIndex >= prefixLowestIndex before it reads the prefix candidate. When the prefix is small, which is the usual case for small inputs with an attached dictionary, this branch is hard to predict. Apply the method of #4144 / #4165 (noDict) to the three prefix checks (long at ip, short at ip, long at ip+1): read through ZSTD_selectAddr(), which returns a local dummy buffer for an invalid index, and test the index only after the bytes compare equal. The index test still decides the result, so the compressed output is unchanged. silesia.tar compressed in independent 1-16 KB blocks with a 16 KB raw dictionary, AMD EPYC 9654, median of 10-12 alternating pairs: clang 14: levels 3-4 +14% to +17% gcc 11: levels 3-4 +2.6% to +4.0% Branch misses drop by 35% (gcc) and 65% (clang). Streaming with an attached dictionary and unknown size, on 1 MB inputs, is +2.4% to +4.7%. Without a dictionary, or with blocks > 16 KB, this code does not run and the results are neutral. Code size: text +1320 bytes (gcc), +2944 bytes (clang). Co-Authored-By: Claude Opus 5.5 --- lib/compress/zstd_double_fast.c | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/compress/zstd_double_fast.c b/lib/compress/zstd_double_fast.c index 1a266e7d955..5cdb3d4eb45 100644 --- a/lib/compress/zstd_double_fast.c +++ b/lib/compress/zstd_double_fast.c @@ -359,6 +359,10 @@ size_t ZSTD_compressBlock_doubleFast_dictMatchState_generic( const U32 dictHBitsL = dictCParams->hashLog + ZSTD_SHORT_CACHE_TAG_BITS; const U32 dictHBitsS = dictCParams->chainLog + ZSTD_SHORT_CACHE_TAG_BITS; const U32 dictAndPrefixLength = (U32)((ip - prefixLowest) + (dictEnd - dictStart)); + /* Array of ~random data, should have low probability of matching data. + * We load from here instead of from the prefix, if a prefix match index + * is invalid. Used to avoid unpredictable branches. */ + const BYTE dummy[] = {0x12,0x34,0x56,0x78,0x9a,0xbc,0xde,0xf0,0xe2,0xb4}; DEBUGLOG(5, "ZSTD_compressBlock_doubleFast_dictMatchState_generic"); @@ -413,7 +417,13 @@ size_t ZSTD_compressBlock_doubleFast_dictMatchState_generic( goto _match_stored; } - if ((matchIndexL >= prefixLowestIndex) && (MEM_read64(matchLong) == MEM_read64(ip))) { + /* matchIndexL >= prefixLowestIndex is an unpredictable branch while the + * prefix is small, e.g. for small inputs with an attached dictionary. + * Read from a safe address instead, and test the index only when the + * bytes match. That is rare, so both branches are well predicted. + * The result is the same as testing the index first. */ + if ( (MEM_read64(ZSTD_selectAddr(matchIndexL, prefixLowestIndex, matchLong, &dummy[0])) == MEM_read64(ip)) + && (matchIndexL >= prefixLowestIndex) ) { /* check prefix long match */ mLength = ZSTD_count(ip+8, matchLong+8, iend) + 8; offset = (U32)(ip-matchLong); @@ -432,12 +442,12 @@ size_t ZSTD_compressBlock_doubleFast_dictMatchState_generic( goto _match_found; } } - if (matchIndexS > prefixLowestIndex) { - /* short match candidate */ - if (MEM_read32(match) == MEM_read32(ip)) { - goto _search_next_long; - } - } else if (dictTagsMatchS) { + /* short match candidate, same method as above */ + if ( (MEM_read32(ZSTD_selectAddr(matchIndexS, prefixLowestIndex, match, &dummy[0])) == MEM_read32(ip)) + && (matchIndexS > prefixLowestIndex) ) { + goto _search_next_long; + } + if (dictTagsMatchS && (matchIndexS <= prefixLowestIndex)) { /* check dictMatchState short match */ U32 const dictMatchIndexS = dictMatchIndexAndTagS >> ZSTD_SHORT_CACHE_TAG_BITS; match = dictBase + dictMatchIndexS; @@ -462,8 +472,9 @@ size_t ZSTD_compressBlock_doubleFast_dictMatchState_generic( const BYTE* matchL3 = base + matchIndexL3; hashLong[hl3] = curr + 1; - /* check prefix long +1 match */ - if ((matchIndexL3 >= prefixLowestIndex) && (MEM_read64(matchL3) == MEM_read64(ip+1))) { + /* check prefix long +1 match, same method as above */ + if ( (MEM_read64(ZSTD_selectAddr(matchIndexL3, prefixLowestIndex, matchL3, &dummy[0])) == MEM_read64(ip+1)) + && (matchIndexL3 >= prefixLowestIndex) ) { mLength = ZSTD_count(ip+9, matchL3+8, iend) + 8; ip++; offset = (U32)(ip-matchL3);