Skip to content

perf: widen AVX2 percentile scan to 16 int64/iter (vector accumulator) - #138

Open
fcostaoliveira wants to merge 5 commits into
HdrHistogram:mainfrom
fcostaoliveira:perf/avx2-percentile-scan-widen16
Open

fcostaoliveira wants to merge 5 commits into
HdrHistogram:mainfrom
fcostaoliveira:perf/avx2-percentile-scan-widen16

Conversation

@fcostaoliveira

Copy link
Copy Markdown
Contributor

Summary

Widen the AVX2 percentile scan (get_value_from_idx_up_to_count_avx2, added in #134) from
4 → 16 int64 per 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-exit
branch. 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 the
GPR extracts and the branch run 4× less often. The four loads and three vpaddq pipeline on
the load/ALU ports.

Only the block granularity changes:

  • the scalar fallback (#ifndef HDR_HAS_AVX2_DISPATCH) is untouched;
  • the uint64_t chunk-sum overflow hardening is preserved;
  • the per-element fine walk (taken only for the single block that crosses the target) and the
    scalar remainder tail are unchanged;
  • results are bit-identical to the previous scan.

Benchmark

test/hdr_percentile_benchhdr_value_at_percentile throughput, best of 20 runs after 3
warmups, pinned to one core, measured base vs patch back-to-back in the same session on an
Intel Xeon Gold 6248 (Cascade Lake):

Compiler Base (main) This PR Δ
gcc 11.4 0.16 M q/s 0.38 M q/s +137%
clang 14.0 0.18 M q/s 0.44 M q/s +144%

The benchmark's sink accumulator 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

# base (main) and patch, each:
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DHDR_HISTOGRAM_BUILD_PROGRAMS=ON -DHDR_HISTOGRAM_BUILD_BENCHMARK=ON
cmake --build build -j
taskset -c 8 ./build/test/hdr_percentile_bench     # compare M queries/sec + sink

Correctness

  • ctest green (gcc and clang).
  • ASan + UBSan clean; the 16-wide loop only runs for idx < (counts_len & ~15), so the widest
    load counts[idx..idx+15] stays in bounds; the remainder is handled by the existing scalar tail.
  • Identical hdr_value_at_percentile output verified via the byte-identical benchmark sink above.

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 to
this change (this PR does not alter the direct-counts[] read introduced in #134).

fcostaoliveira pushed a commit to redis-performance/hdr-agent-workspace that referenced this pull request Jul 1, 2026
…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).
@fcostaoliveira

Copy link
Copy Markdown
Contributor Author

⚠️ Conflict heads-up: this enhances the AVX2 percentile scan, whereas my open #137 proposes to remove the AVX2 dispatch entirely (portable scalar block-sum). #138/#139 and #137 are two directions for the same read path — they conflict. Flagging for coordination; happy to pick one.

fcostaoliveira pushed a commit to fcostaoliveira/HdrHistogram_c that referenced this pull request Jul 2, 2026
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).
fcostaoliveira pushed a commit to fcostaoliveira/HdrHistogram_c that referenced this pull request Aug 28, 2026
…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.
@fcostaoliveira
fcostaoliveira force-pushed the perf/avx2-percentile-scan-widen16 branch from 673d52e to 7766d40 Compare August 28, 2026 07:58
fcostaoliveira pushed a commit to fcostaoliveira/HdrHistogram_c that referenced this pull request Aug 28, 2026
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).
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

🤖 Automated first-pass review — a human maintainer's review is still required before merge.

Checked the bounds reasoning rather than taking it on faith. counts_len = (bucket_count + 1) * sub_bucket_half_count (hdr_histogram.c:434), and sub_bucket_half_count is 2^sub_bucket_half_count_magnitude with that magnitude ≥ 4 for every significant_figures hdr_calculate_bucket_config will accept (1–5 → half counts of 16/128/1024/16384/131072). So counts_len is always a multiple of 16, limit = counts_len & ~15 never truncates, and both the widest load and the idx + 16 fine walk stay in bounds. That also means the scalar remainder tail stays unreachable in practice, exactly as it was under & ~3 — widening the potential remainder from 3 to 15 elements costs nothing real.

The uop accounting is consistent with the claimed numbers (roughly 1.75 → 0.8 uops/element once the two vpextrq and the early-exit branch run once per 16 instead of once per 4), and hdr_percentile_bench is in-tree, so the repro steps actually run as written.

Two pre-existing things this doesn't change, flagged only so they aren't mistaken for being covered by the new test:

  • The scan still reads h->counts[idx] directly rather than going through hdr_count_at_index(), so it ignores normalizing_index_offset. The new test builds its reference from hdr_count_at_index(), but only over a freshly hdr_init'd histogram where the offset is zero, so it cannot detect that divergence. You note this in the description; worth being explicit that the test doesn't close it, and that Java's AbstractHistogram does apply the offset at this point. If perf: block-summed scalar percentile scan (non-AVX2 fallback) + offset-safe dispatch #137 is what restores it, the ordering of the two matters more than either change does on its own.
  • The (uint64_t) cast on the chunk sum exists to avoid signed-overflow UB "if invariants are violated", but running + chunk on the next line is still a plain signed add, and if that predicate fires without the fine walk returning, control falls through to running += chunk after the walk has already advanced running across the block. Neither is reachable while the total_count invariant holds — the hardening is just half-applied, and it came in with perf: AVX2 vectorized prefix-sum in percentile scan #134 rather than here.

No public header, struct hdr_histogram layout or existing function signature is touched, so no SOVERSION question arises. The added test exercises the AVX2 path on CI's x64 legs and the scalar path on the 32-bit x86 legs (where HDR_HAS_AVX2_DISPATCH is off per #143), which is the coverage you'd want for this change.

fcostaoliveira and others added 4 commits September 2, 2026 12:31
…_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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant