perf: block-summed scalar percentile scan (non-AVX2 fallback) + offset-safe dispatch - #137
fcostaoliveira wants to merge 3 commits into
Conversation
91fb98b to
c644509
Compare
filipecosta90
left a comment
There was a problem hiding this comment.
The force-push at 91fb98b3 drops a correctness fallback that hdr_value_at_percentiles had pre-PR. Flagging in case it's unintentional.
The regression
The old hdr_value_at_percentiles walked buckets via hdr_iter_next, which dereferences counts through counts_get_normalised (src/hdr_histogram.c:822) — i.e. honors h->normalizing_index_offset. The new single-pass code reads h->counts[idx] directly. When normalizing_index_offset != 0 the bucket layout is rotated in counts[], so the cumulative scan crosses the target at the wrong physical bucket and hdr_value_at_percentiles returns the wrong percentile values.
The single-percentile hdr_value_at_percentile already had this issue pre-PR (via get_value_from_idx_up_to_count), so that one is not a regression — it's pre-existing. The batched API is the one this PR newly breaks.
When it triggers
normalizing_index_offset is set non-zero only by the decode paths in hdr_histogram_log.c:540,642 (hdr_log_read / hdr_decode_compressed) when the wire format carries a non-zero offset. The C library encoder only emits what it sees (always 0 from hdr_init), so the trigger is realistic only for cross-language interop (e.g. decoding a histogram emitted by Java HdrHistogram after a shiftValuesLeft/Right) or other encoders. Corner-case in C-only deployments; load-bearing for anyone reading wire-format logs from polyglot pipelines.
Suggested fixes (any one)
- Add an offset-aware fallback at the top of both scan functions — what
c644509(force-pushed away on this branch) did. Fast path stays identical; theHDR_UNLIKELY(h->normalizing_index_offset != 0)branch walks viacounts_get_normalised. Preserves the perf claim. - Hoist
normalizing_index_offsetinto the inner loop viacounts_get_normalisedunconditionally. Slower across the board; the compiler may or may not LICM-hoist the offset==0 check out of the loop. - Document the precondition on
hdr_value_at_percentiles("undefined behavior on histograms with non-zeronormalizing_index_offset") and add anassert. Cheapest, but silently changes the contract.
Happy with any of these — option 1 is what I'd pick (preserves both correctness and perf). If you're OK with the change of contract, option 3 is fine too as long as it's documented in the header alongside the non-decreasing-percentiles note.
Side notes
- The
running += block_summade unconditional (with the new comment) is a nice tweak; compiler can schedule independently of the crossing-branch. - The
uint64_tcast hardening on the block sum (added during PR #134's review to guard signed-overflow UB on fuzzed/corrupted histograms) was also dropped in the force-push. Restoring is a one-liner — doesn't change valid-state behavior; just keeps the prior intent for fuzz robustness. HDR_UNLIKELYon the cold crossing branch matches the file's convention introduced by recent PRs. Not load-bearing.
|
Good catch, and correct on all counts — the
On the Cross-µarch numbers for the |
|
|
e569f10 to
9992428
Compare
…t-safe dispatch The scalar get_value_from_idx_up_to_count path (used on every non-x86 target, where the AVX2 dispatch compiles out) tested the cumulative count after every single add — a loop-carried dependency that serializes the walk. Replace it with a block-summed scan (BLK=4): sum a small fixed block, test the running total once per block, and enter the precise per-element walk only for the block that crosses the target. On ARM Neoverse-V2 this raises hdr_value_at_percentile throughput from 0.12 to 0.26 M q/s (+117%); x86 is unchanged (still takes the AVX2 path). Also make the dispatch offset-safe: the AVX2 (and old scalar) scans read counts[] directly, which is wrong for a non-zero normalizing_index_offset (decoded/rotated histograms). Route offset != 0 to the scalar scan, whose fallback uses the offset-aware counts_get_normalised accessor. Composes with the AVX2-widening PRs (HdrHistogram#138/HdrHistogram#139): x86 keeps the widened SIMD scan, non-x86 gets the block-summed scalar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9992428 to
d619ead
Compare
|
🤖 Automated first-pass review — a human maintainer's review is still required before merge. The offset-safe routing looks like the real substance here, more than the perf. Every other read path ( The same defect appears to survive two lines below the new clamp: The decode clamp is the highest-value change in the diff and the only one with no test: the added test pokes On the block-summed scalar: for Last, scope: the perf change, the dispatch correctness fix, and the decoder bounds fix are three independent things. The latter two may be worth landing separately if they'd want to go out without the perf change attached. |
…_len) An out-of-range offset from an untrusted log defeats normalize_index's single +/-counts_len wrap and indexes counts[] out of bounds on the offset-aware read paths (now including the percentile scan). Reduce it at decode; no-op for valid logs where |offset| < counts_len.
What
Replaces the scalar
get_value_from_idx_up_to_count(the non-AVX2 percentile-scan fallback) with a block-summed scan, and makes the dispatch offset-safe.Why
The scalar scan tested the cumulative count after every single add — a loop-carried dependency that serializes the walk. It runs on every non-x86 target (where the AVX2 dispatch compiles out) and for decoded/rotated histograms. The block-summed scan (BLK=4) sums a small fixed block, tests the running total once per block, and enters the precise per-element walk only for the block that crosses the target.
Separately, the AVX2 (and the old scalar) scan read
counts[]directly, which is wrong whennormalizing_index_offset != 0(decoded / rotated histograms). The dispatcher now routesoffset != 0to the scalar scan, whose fallback uses the offset-awarecounts_get_normalisedaccessor.Results —
hdr_percentile_benchbest M queries/sec (identicalsink, i.e. bit-identical results)main)x86 is untouched (still takes the AVX2 path) and this composes with the AVX2-widening PRs #138/#139 — on x86 those take the scan to ~+125% while non-x86 gets the block-summed scalar. The head-to-head that motivated this split (portable-only #137 gave only +7–12% on x86 vs AVX2's +83–125%, but +117% on ARM where AVX2 is absent) is why the two are kept orthogonal.
Correctness
ctest5/5; ASan+UBSan clean (no real errors). Foroffset == 0the block-sum is a straight prefix-sum equivalent to the old scalar (the percentile assertions pass); foroffset != 0it now uses the offset-aware accessor — strictly more correct than the previous direct-counts[]read. Unsigned block accumulation avoids signed-overflow UB on corrupted/fuzzed state.🤖 Generated with Claude Code