fix(dictBuilder): keep dictItem slot index valid across the merge fixpoint - #4724
Open
teerthsharma wants to merge 1 commit into
Open
fix(dictBuilder): keep dictItem slot index valid across the merge fixpoint#4724teerthsharma wants to merge 1 commit into
teerthsharma wants to merge 1 commit into
Conversation
…point ZDICT_insertDictItem identifies the absorbed entry by its slot index while iterating ZDICT_tryMerge to a fixpoint. ZDICT_tryMerge ends each successful merge with a rank-improving insertion sort, which moves every entry between the absorbing entry's old and new slot one slot back. When the caller's index lies in that range it no longer denotes the absorbed entry, so the following ZDICT_removeDictItem deletes an unrelated live segment and the absorbed entry survives as a duplicate. Separately, ZDICT_removeDictItem shifts every entry above the removed slot one slot forward, and newMerge is carried into the next round of the fixpoint without that adjustment. eltNbToSkip becomes a U32*. Each rank sort reports whether it displaced that slot, and the fixpoint re-reads the slot after both the sort and the removal. Reproduction: insert (pos,length,savings) = (1000,10,200), (340,20,100), (300,20,90), (320,30,60). The fourth item bridges the second and third, the cascading merge lifts the absorbing entry from slot 3 to slot 1, and the segment at position 1000 -- which takes part in no merge and lies 700 bytes from the nearest other interval -- is deleted. Only ZDICT_trainFromBuffer_legacy and the CLI's --train-legacy reach this code. ZDICT_trainFromBuffer routes to ZDICT_optimizeTrainFromBuffer_fastCover and is unaffected. There is no public API change, no dictionary format change, and no change to the ranking heuristic or the savings computation. Signed-off-by: teerth sharma <teerths57@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ZDICT_insertDictItembuilds the dictionary segment table by fixpoint. EachdictItemis an interval[pos, pos + length)in the sample buffer;ZDICT_tryMergefuses the candidate with one overlapping entry, and the loopre-runs because fusing two intervals can produce one that reaches a third.
Across that loop the absorbed entry is identified by its array slot,
mergeId.ZDICT_tryMergeends each successful merge with a rank-improving insertion sort(
zdict.c:377-380andzdict.c:396-400). Merging raisessavings, so theabsorbing entry moves toward the front of the savings-ranked table and every
entry between its old and new slot shifts one slot back — including, on some
paths, the entry
mergeIdnames. The followingZDICT_removeDictItem(table, mergeId)then deletes an unrelated live segment, and the absorbed entrysurvives as a duplicate. Separately,
ZDICT_removeDictItemshifts every entryabove
mergeIdone slot forward, andnewMergeis carried into the next roundof the fixpoint without that adjustment.
This change makes
eltNbToSkipaU32*. Each rank sort reports whether itdisplaced that slot, and the fixpoint re-reads the slot after both the sort and
the removal.
There is no public API change, no dictionary format change, and no change to
the ranking heuristic, the savings computation,
ZDICT_analyzePos, or thecover/fastcover trainers. Only
ZDICT_trainFromBuffer_legacyand the CLI's--train-legacyreach the modified code;ZDICT_trainFromBufferroutes toZDICT_optimizeTrainFromBuffer_fastCoverand is unaffected.Reproduction
Four inserts into a freshly initialised table, items given as
(pos, length, savings):The fourth item tail-overlaps
(340, 20), which then front-overlaps(300, 20). The second merge lifts the absorbing entry from slot 3 to slot 1,shifting slots 1 and 2 back; the caller still holds slot 2.
devat 82d322c(pos=300 len=60 savings=229)(pos=300 len=60 savings=238),(pos=1000 len=10 savings=200)The segment at position 1000 takes part in no merge and is deleted. Both rows
are produced by the same program built against each tree with gcc 15.2.0 on
Ubuntu 26.04 LTS under WSL2.
Validation
From PR head
fc2b6a358on base82d322c49, gcc 15.2.0, Ubuntu 26.04 LTS underWSL2, built with the project's own test flags (which include
-Wdeclaration-after-statementand-Wc++-compat):tests/fuzzer -i3000tests/zstreamtest -i1000zstd --train-legacyover 49 headers fromlib/, then compress and decompress each against the resulting 55,860 B dictionaryAn earlier run of the same three checks at
tests/fuzzer -i5000andtests/zstreamtest -i2000also completed with exit 0.lib/dictBuilder/zdict.ccompiles with no new diagnostics under-Wall -Wextra -Wconversion -Wcast-qual -Wstrict-prototypes; the warnings thefile already emits are at
zdict.c:596,:905and:989, none of them onchanged lines.
Frequency
An executable port of
zdict.c:137-545driven over this repository's ownlib/directory counts every
ZDICT_removeDictItemcall whose victim differs from theentry the caller selected, comparing
(pos, length)before and after:At 48 KB the segment count is unchanged because each destroyed segment is
replaced by a duplicate of another; at 96 KB and above the change retains
strictly more distinct segments.
Dictionary effect
Dictionaries built through
ZDICT_trainFromBuffer_legacyfromlib/andprograms/: 57 training files totalling 2,359,503 B, 57 held-out filestotalling 1,340,695 B, dictionary capacity 112,640 B,
selectivityLevel9.Both trees fill the capacity exactly.
devat 82d322cb95f42d334a56617Held-out compression moves -75 B at level 3 and +40 B at level 9, both under
0.03% and in opposite directions.
Existing coverage
tests/fuzzer.cexercisesZDICT_trainFromBuffer(fuzzer.c:3050,fuzzer.c:3058,fuzzer.c:3648,fuzzer.c:4474) andZDICT_trainFromBuffer_cover(fuzzer.c:3790). It does not callZDICT_trainFromBuffer_legacy.tests/playTests.shinvokes--train-legacyonly in two negative cases that assert failure on insufficient and on pure-noise
input (
playTests.sh:1165,playTests.sh:1167). The legacy trainer has nopositive assertion on its output in the test suite.
Limits
The merge machinery and the segment table are file-static and not reachable from
any public or static-API entry point, so the reproduction above requires
including
zdict.cdirectly and does not fit the pattern used by the existingtests; no regression test is included for that reason. The change alters emitted
dictionary bytes on inputs that trigger the defect, so any consumer pinning a
legacy-trained dictionary checksum will observe a difference. Held-out
compression ratio is not the argument for this change and the measured effect is
within noise in both directions; the argument is that the fixpoint deletes an
entry it did not select. All measurements were taken on a single machine, and
no 32-bit target was exercised.
Fixes #4723.