Skip to content

NEON-vectorized intersection2by2 - #543

Open
gitRasheed wants to merge 4 commits into
RoaringBitmap:masterfrom
gitRasheed:neon-intersect2by2
Open

gitRasheed wants to merge 4 commits into
RoaringBitmap:masterfrom
gitRasheed:neon-intersect2by2

Conversation

@gitRasheed

@gitRasheed gitRasheed commented Aug 9, 2026

Copy link
Copy Markdown

NEON implementations of intersection2by2 and intersection2by2Cardinality for arm64. Arrays where both sides have at least 16 elements run through a 128-bit SIMD kernel. Smaller or heavily skewed inputs keep the existing scalar code.

NEON has no cmpestrm (the gap noted in #288), so the kernel compares each set1 block against rotations of set2's block, reduces the result with a two-multiply movemask, and compacts matched lanes through the shuffle table. Blocks with no matches skip the mask extraction. A range gate fast-forwards disjoint runs on scalar boundary loads, and the galloping search takes over above a 32:1 size ratio. For the in-place caller, when output may have overwritten the current set1 block, the kernel hands it back in registers and the wrapper finishes from that copy. The boolean intersects2by2 stays scalar because its early exit beats fixed vector work.

Intersection had no vector path on any architecture here, and it is one of the top set-operation costs in real-roaring-datasets profiles.

I made a page that steps through this kernel on worked examples and annotates the assembly line by line, to make the design choices easier to follow: https://claude.ai/code/artifact/5f77af0d-89c2-4965-9d15-95fde361c47f#intersection (the other tabs cover the other three kernels).

Changes

  • setutil_arm64.go: the lane-compaction table, its own commit, shared with NEON-vectorized union2by2 #567, NEON-vectorized difference #568 and NEON-vectorized exclusiveUnion2by2 #569.
  • setutil.go / setutil_generic.go: difference, exclusiveUnion2by2 and the two intersection entry points get the generic/arm64 split that union2by2 already has. This commit is shared with NEON-vectorized difference #568 and NEON-vectorized exclusiveUnion2by2 #569 so the three merge in any order, and it drops out of the others once one of them lands; the two passthrough files for the other operations are part of it.
  • setutil_intersect_neon_arm64.s: the two kernels.
  • setutil_intersect_arm64.go: dispatch and the spill drain.
  • Differential tests: exact-capacity canaries, the iandArray in-place aliasing geometry, three-way self-intersection, duplicate-input no-panic bounds, misalignment offsets, the galloping boundary at 32:1.
  • BenchmarkIntersect2By2 and BenchmarkIntersectCard2By2, rotating eight seeded datasets between iterations.
  • TestAndSelfInPlace pins Bitmap.And with the same receiver.

Testing

go test passes for all packages on Graviton 4. The earlier version of the kernel also passed on Graviton 2 and Apple M1. go fmt clean. Smat fuzz (through the wrapper noted in #538): 585,864 executions, no failures.

Performance

Graviton 4 (c8g.xlarge). BenchmarkIntersect2By2 from this PR, dispatch against scalar:

BenchmarkIntersect2By2/dense50/16/dispatch          19.3 ns/op
BenchmarkIntersect2By2/dense50/16/scalar            27.0 ns/op
BenchmarkIntersect2By2/dense50/64/dispatch          52.5 ns/op
BenchmarkIntersect2By2/dense50/64/scalar           104.7 ns/op
BenchmarkIntersect2By2/dense50/256/dispatch        182.6 ns/op
BenchmarkIntersect2By2/dense50/256/scalar          447.6 ns/op
BenchmarkIntersect2By2/dense50/4096/dispatch        2823 ns/op
BenchmarkIntersect2By2/dense50/4096/scalar         23762 ns/op
BenchmarkIntersect2By2/skew8/4096/dispatch         28382 ns/op
BenchmarkIntersect2By2/skew8/4096/scalar           52158 ns/op
BenchmarkIntersectionLargeRoaring    3.89ms -> 1.00ms   (3.9x)
BenchmarkIntersectionLargeParallel   4.63ms -> 1.56ms   (3.0x)

The last commit (five rotations instead of seven, no-match exit, galloping from 32:1) on its own: dense 4096 materialization 2830 to 2658 ns, cardinality 2081 to 1953 ns, 32 against 2016 elements 558 to 354 ns. Real-roaring-datasets geomean over twelve datasets: And 15 percent and AndCardinality 36 percent faster than scalar as loaded, 8 and 17 percent after RunOptimize.

The floor of 16 is where every measured shape wins on Neoverse N1, V2 and Apple M1.

@gitRasheed
gitRasheed force-pushed the neon-intersect2by2 branch 3 times, most recently from b3938c9 to 73c73ad Compare August 10, 2026 20:55
@gitRasheed

Copy link
Copy Markdown
Author

btw, while working on this kernel, I noticed that Go's Validate allows adjacent duplicates because it only checks previous > next; CRoaring, Java, and Rust all require strictly increasing values and reject them with previous >= next. I ended up having to bound the assembly stores to cope with those duplicates, so is Go's looser validation intentional, or should Validate reject them instead?

@lemire

lemire commented Aug 11, 2026

Copy link
Copy Markdown
Member

@gitRasheed Thanks. You are correct. I fixed this with a subsequent PR.

@gitRasheed

Copy link
Copy Markdown
Author

Hi @lemire , is there anything I can do here or in #538 to help get them merged?

@lemire

lemire commented Aug 17, 2026

Copy link
Copy Markdown
Member

@gitRasheed I assess all PRs manually, so it takes time. Sorry.

@gitRasheed

Copy link
Copy Markdown
Author

@lemire no rush, just wanted to make sure I wasn't missing something, asking because I also have NEON AndNot PR ready but didn't want to spam open a bunch of PRs on you, fine if I send that over as well soon?

union2by2 already has a generic definition and an arm64 one. Give
difference, exclusiveUnion2by2, intersection2by2 and
intersection2by2Cardinality the same split so vector versions can land
one at a time without touching the shared files. The arm64 versions
pass through to the scalar code for now.
@gitRasheed

gitRasheed commented Sep 11, 2026

Copy link
Copy Markdown
Author

Rebased on current master and force-pushed. The description is rewritten for the new branch: shuffle table and dispatch split as separate commits, five rotations instead of seven in the match core, galloping from 32:1. Numbers are in the description.

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