perf: widen AVX2 percentile scan to 16 int64/iter (vector accumulator) - #138
fcostaoliveira wants to merge 5 commits into
Conversation
…hmark data Opened HdrHistogram/HdrHistogram_c#138 (fcostaoliveira: perf/avx2-percentile-scan-widen16 -> HdrHistogram:main, +15/-7, MERGEABLE). Body carries the clx1 same-session A/B table (read +137% gcc / +144% clang), repro via hdr_percentile_bench, correctness (bit-identical sink, ctest, ASan/UBSan), and the #137 relationship. Logs synced (EXPERIMENTS/SUMMARY/README/memory).
The widened AVX2 percentile scan is memory-load-latency bound over the ~10s-of-KB counts[] array. Prefetch 4 iterations (512 B) ahead with _MM_HINT_T0 to hide L2/L3 latency. Read throughput (hdr_value_at_percentile), same-session core-pinned A/B: Cascade Lake (Xeon Gold 6248): gcc +8%, clang neutral Granite Rapids: gcc +7.7%, clang +5.7% Write path unaffected (control flat on both); percentile results bit-identical. Stacked on perf/avx2-percentile-scan-widen16 (PR HdrHistogram#138).
…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>
…ator get_value_from_idx_up_to_count_avx2 summed 4 int64/iter and did a horizontal reduction + 2x _mm_extract_epi64 + target-cross branch every 4 elements. Accumulate 16 int64/iter (4x256) in a vector register and reduce to a scalar block sum once per 16, so the costly GPR extracts and the early-exit branch run 4x less often. Scalar fallback and uint64 overflow hardening unchanged; percentile results bit-identical. clx1 (Cascade Lake), core-pinned, same-session A/B: hdr_value_at_percentile +137% (gcc 0.16->0.38 Mq/s) / +144% (clang 0.18->0.44 Mq/s). Read sink byte-identical.
673d52e to
7766d40
Compare
The widened AVX2 percentile scan is memory-load-latency bound over the ~10s-of-KB counts[] array. Prefetch 4 iterations (512 B) ahead with _MM_HINT_T0 to hide L2/L3 latency. Read throughput (hdr_value_at_percentile), same-session core-pinned A/B: Cascade Lake (Xeon Gold 6248): gcc +8%, clang neutral Granite Rapids: gcc +7.7%, clang +5.7% Write path unaffected (control flat on both); percentile results bit-identical. Stacked on perf/avx2-percentile-scan-widen16 (PR HdrHistogram#138).
|
🤖 Automated first-pass review — a human maintainer's review is still required before merge. Checked the bounds reasoning rather than taking it on faith. The uop accounting is consistent with the claimed numbers (roughly 1.75 → 0.8 uops/element once the two Two pre-existing things this doesn't change, flagged only so they aren't mistaken for being covered by the new test:
No public header, |
…_reference hdr_init makes two allocations - the counts array and the histogram struct - so free(h) frees only the struct and leaks counts (188416 bytes here). hdr_close frees both, and is what the rest of this file already uses. The sanitizers job added in HdrHistogram#145 enables LeakSanitizer, so this test-only leak now fails CI on this branch; it was written before that job existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
Widen the AVX2 percentile scan (
get_value_from_idx_up_to_count_avx2, added in #134) from4 → 16
int64per iteration using a vector accumulator.The current loop processes 4 counts per iteration and, every iteration, does a horizontal
reduction plus two
_mm_extract_epi64(vector→GPR moves) plus the running-total early-exitbranch. Those are the expensive parts. This PR accumulates 16 counts per iteration (4×256-bit
loads summed in a
__m256i) and reduces to a scalar block sum once per 16 elements, so theGPR extracts and the branch run 4× less often. The four loads and three
vpaddqpipeline onthe load/ALU ports.
Only the block granularity changes:
#ifndef HDR_HAS_AVX2_DISPATCH) is untouched;uint64_tchunk-sum overflow hardening is preserved;scalar remainder tail are unchanged;
Benchmark
test/hdr_percentile_bench—hdr_value_at_percentilethroughput, best of 20 runs after 3warmups, pinned to one core, measured base vs patch back-to-back in the same session on an
Intel Xeon Gold 6248 (Cascade Lake):
The benchmark's
sinkaccumulator is byte-identical between base and patch on both compilers(
17401860284404480), i.e. every percentile query returns exactly the same value as before.Steps to reproduce
Correctness
ctestgreen (gcc and clang).idx < (counts_len & ~15), so the widestload
counts[idx..idx+15]stays in bounds; the remainder is handled by the existing scalar tail.hdr_value_at_percentileoutput verified via the byte-identical benchmarksinkabove.Relationship to #137
This optimizes the existing (#134) AVX2 path and is independent of #137. If you prefer #137's
portable scalar block-sum (dropping the AVX2 dispatch), I'm happy to re-target — I can send the
same 16-wide vector-accumulator idea rebased on that, or the equivalent widening of the portable
block loop. Whichever direction you'd like, just say the word. Note #137 also restores the
normalizing_index_offset-aware fallback for decoded histograms; that behavior is orthogonal tothis change (this PR does not alter the direct-
counts[]read introduced in #134).