Skip to content

probe: does vectorizing r2sleigh's OpColumns scans pay? Measured — mostly no - #308

Merged
AdaWorldAPI merged 2 commits into
masterfrom
claude/c64-6502-falsifier-shztkk
Sep 14, 2026
Merged

AdaWorldAPI merged 2 commits into
masterfrom
claude/c64-6502-falsifier-shztkk

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Answers the question r2il::columns asks 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 no ndarray dependency 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-census fixture (PE32+, .text 7 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:

arm conjunction passes
S scalar over the native u8/u64 columns, no widening 1
AND 4 *_to_mask + 3 mask_and_assign 7
TERN 4 *_to_mask + mask_ternlog::<AND3> + mask_and_assign 6
UNDER the _under chain — each predicate narrows the live mask 4

S is 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)

span S AND TERN UNDER S/AND S/TERN S/UNDER
1 024 0.78 0.51 0.50 0.60 1.54 1.56 1.30
4 096 0.76 0.54 0.53 0.57 1.42 1.45 1.33
12 408 (real) 0.70 0.52 0.53 0.49 1.35 1.32 1.42
49 632 T 0.70 0.58 0.58 0.55 1.21 1.22 1.29
198 528 T 0.87 0.89 0.88 0.67 0.98 0.99 1.31
794 112 T 0.87 1.12 1.11 0.90 0.77 0.78 0.96
3 176 448 T 0.91 1.22 1.13 0.95 0.75 0.80 0.95

Spans past 12 408 are the real stream tiled — labelled T, and evidence about throughput only, never about program shape.

Four findings

  1. The crossover is low. The mask arms win to ~50 K ops and lose from ~200 K. No bandwidth surprise: S reads 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.
  2. The ternlog fusion is not the lever for this query. 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 where a caller already holds the masks.
  3. _under is 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.
  4. The ratio is favourable exactly where the absolute time is irrelevant. A whole-census scan is 8.9 µs scalar vs 6.4 µs vectorized — 2.5 µs saved on a binary whose SLEIGH lift costs milliseconds. Per the workspace rule a word-level op pays for the span it is given, this span is not worth paying for.

Two primitive gaps, worked around and recorded rather than closed

  • No u8 comparator. OpColumns::{tag,space} are Vec<u8>; the facade's narrowest value type is u32, so a consumer keeps a widened copy at 4× the bytes of the column it scans.
  • No u64 range comparator. ternary_match_u64_to_mask is exact-with-don't-care; the ordered family stops at i32. Measured, 100 % of Ram-space offsets exceed 2³² (image-based, 0x1_4000_105e0x1_4000_8398), so narrowing is not sound in general. The query is re-expressed exactly by splitting hi32/lo32 — valid only because the window lies inside one hi32 bucket, which the probe asserts rather than assumes.

Whether to add ge/lt_u64_to_mask is a decision, not a drive-by: this PR adds no primitive, changes no default, and touches nothing outside examples/.

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

  • New Features
    • Added a release-mode scan benchmark for column dumps.
    • Compares scalar and SIMD-style scan strategies across tag lookups and RAM-offset range queries.
    • Reports matching results, execution time per operation, and relative performance ratios.
    • Verifies that optimized scan results match the scalar reference.

…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
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: c6124c43-6102-41e5-a5be-e77a89b8bb21

📥 Commits

Reviewing files that changed from the base of the PR and between 854924a and 0471cdd.

📒 Files selected for processing (2)
  • Cargo.toml
  • examples/r2il_column_scan_probe.rs

📝 Walkthrough

Walkthrough

The PR adds the r2il_column_scan_probe example. It loads real or tiled column dumps, compares scalar and SIMD-style scans, validates matching results, and reports timing ratios.

Changes

Column scan benchmark

Layer / File(s) Summary
Probe registration and input setup
Cargo.toml, examples/r2il_column_scan_probe.rs
Registers the std-gated example and prepares real or tiled column-dump inputs.
Scan predicates and correctness validation
examples/r2il_column_scan_probe.rs
Adds scalar reference scans, widened byte columns, split offset columns, mask implementations, and bit-for-bit correctness checks.
Benchmark timing and tag comparison
examples/r2il_column_scan_probe.rs
Measures scalar and mask scans, then compares native u8 tag lookup with widened u32 equality scanning.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Other

Suggested reviewers: claude

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

A rabbit reads each line,
The patch grows clear beneath the moon,
Small changes hop in place,
Tests guard the garden path,
Reviews bloom before the dawn.

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Sep 14, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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
@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review September 14, 2026 21:01
@AdaWorldAPI
AdaWorldAPI merged commit 7adc98e into master Sep 14, 2026
24 of 25 checks passed
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.

2 participants