probe: does vectorizing r2sleigh's OpColumns scans pay? Measured — mostly no - #308
Conversation
…stly no `r2il::columns` was laid out for this mask surface and named `eq_u32_to_mask` / `masked_strided_group_sum` in its own module docs, while deliberately taking no ndarray dependency and declining to claim the SIMD was worth it: "a profiling question nobody has answered." This answers it from the consumer side, on a real x86-64 lift (12 408 p-code ops from the win32-census fixture) rather than a synthetic stream. Four arms compute an identical mask — scalar over the native u8/u64 columns, 4 predicates + 3 mask_and_assign, the same with one mask_ternlog::<AND3>, and the _under chain — with a bit-for-bit equivalence gate before any timing, swept from 256 ops to 3.2 M. Findings, all in the module doc with the table: - The crossover is low: the mask arms win to ~50 K ops and LOSE from ~200 K (0.75-0.80x at 3.2 M). The scalar arm reads 9 B/op; the mask arms read 12 B/op of widened columns plus four mask buffers. - The ternlog fusion is not the lever here — TERN and AND are within noise at every span. The cost is the four passes over value columns, not the three combines fusion removes. Fusion pays when a caller already HOLDS the masks. - `_under` is the arm that survives scale: no separate combine, no extra buffers, best at the real size (1.42x) and nearest parity at 3.2 M. - The ratio is favourable exactly where the absolute time is irrelevant: 8.9 us scalar vs 6.4 us vectorized for a whole-census scan, on a binary whose SLEIGH lift costs milliseconds. Two primitive gaps the probe had to work around, and both are findings: no u8 comparator (the columns are Vec<u8>; the narrowest value type is u32, so a consumer pays a 4x widened copy) and no u64 RANGE comparator (only exact/ternary match; the ordered family stops at i32). Measured, 100% of Ram-space offsets exceed 2^32, so narrowing is not sound in general — the query is re-expressed exactly here by splitting hi32/lo32, valid only because the window lies in one hi32 bucket, which the probe asserts rather than assumes. No primitive is added and no default changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR adds the ChangesColumn scan benchmark
Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Other Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
A rabbit reads each line, Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_71156dff-918a-42ac-8e73-7d1b9b9bae2e) |
…eady are `tests/1.98.1` went red on `cargo test --no-run --no-default-features`: that job builds examples too, and `ndarray::simd` is `#[cfg(feature = "std")]`. Cargo.toml documents this exact case three lines above the entry added here — "AMX examples import `ndarray::simd` / `ndarray::hpc`, both `#[cfg(feature = "std")]`, so they must be skipped in `--no-default-features` CI jobs" — and `hex_trie_vs_gemm_probe` / `ternlog_amortization_probe` each carry `required-features = ["std"]`. The new probe did not; that is the whole defect. Reproduced the failing job's own command locally (4x E0432/E0433, "could not find `simd` in `ndarray`"), then re-ran it after the fix: exit 0. The default-featured build and the probe's own run are unchanged, and `cargo fmt --check` is clean. `tests/stable` and `tests/beta` were cancelled by the matrix's fail-fast, not independently red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv
Answers the question
r2il::columnsasks and declines to answer itself. Its module doc names this crate's mask surface (eq_u32_to_mask,masked_strided_group_sum), lays the columns out for it, takes nondarraydependency on purpose — and says plainly: "Whether that is worth doing is a profiling question nobody has answered."Measured, consumer-side, on a real x86-64 lift: 12 408 p-code ops from r2sleigh's committed
win32-censusfixture (PE32+,.text7 688 B), not a synthetic stream.Method
Four arms compute an identical mask for the census's own IAT/prefetch query —
space == Ram && lo <= offset < hi— with a bit-for-bit equivalence gate before any timing, swept 256 → 3.2 M ops:Su8/u64columns, no wideningAND*_to_mask+ 3mask_and_assignTERN*_to_mask+mask_ternlog::<AND3>+mask_and_assignUNDER_underchain — each predicate narrows the live maskSis the honest baseline because it needs no widened columns: charging the mask arms for the layout they require is the comparison a consumer actually faces.Result (Xeon @ 2.10 GHz, avx512f/bw/vl, release, 3 runs, ns/op)
Spans past 12 408 are the real stream tiled — labelled
T, and evidence about throughput only, never about program shape.Four findings
Sreads 9 B/op, the mask arms read 12 B/op of widened columns and write four mask buffers. The layout's own motivation — fewer bytes touched — is partly spent paying for the primitives' value types.TERNandANDare within noise at every span. The cost is the four passes over value columns, not the three combines fusion removes. Fusion pays where a caller already holds the masks._underis the arm that survives scale — no separate combine, no extra buffers: best at the real size (1.42×) and the only one near parity at 3.2 M.Two primitive gaps, worked around and recorded rather than closed
u8comparator.OpColumns::{tag,space}areVec<u8>; the facade's narrowest value type isu32, so a consumer keeps a widened copy at 4× the bytes of the column it scans.u64range comparator.ternary_match_u64_to_maskis exact-with-don't-care; the ordered family stops ati32. Measured, 100 % of Ram-space offsets exceed 2³² (image-based,0x1_4000_105e…0x1_4000_8398), so narrowing is not sound in general. The query is re-expressed exactly by splittinghi32/lo32— valid only because the window lies inside onehi32bucket, which the probe asserts rather than assumes.Whether to add
ge/lt_u64_to_maskis a decision, not a drive-by: this PR adds no primitive, changes no default, and touches nothing outsideexamples/.Pairs with AdaWorldAPI/r2sleigh#14 (the env-gated column dump that feeds it).
🤖 Generated with Claude Code
https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv
Summary by CodeRabbit