Skip to content

NEON-vectorized exclusiveUnion2by2 - #569

Open
gitRasheed wants to merge 3 commits into
RoaringBitmap:masterfrom
gitRasheed:perf/neon-xor-shared
Open

gitRasheed wants to merge 3 commits into
RoaringBitmap:masterfrom
gitRasheed:perf/neon-xor-shared

Conversation

@gitRasheed

@gitRasheed gitRasheed commented Sep 11, 2026

Copy link
Copy Markdown

NEON kernel for exclusiveUnion2by2, the array-container xor, on arm64. Both arrays need at least 32 elements. Smaller inputs keep the scalar merge.

Unlike the union kernel, this one carries no partial block between steps. Each step takes a 16-element window from both sides and counts how many elements of each lie at or below the smaller window maximum. It sorts all 32 through a bitonic network and keeps a value only when it differs from both neighbours and lies at or below that maximum. Every step consumes a closed value interval, so a duplicate pair can never straddle two steps and there is no seam state. Windows entirely below the other side are copied straight through, and identical windows cancel without sorting.

Xor had no vector path on any architecture here. The output buffer is always fresh and sized for both inputs, so the kernel needs neither an in-place spill nor a store clamp.

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#xor (the other tabs cover the other three kernels).

Changes

  • setutil_arm64.go: the lane-compaction table, its own commit, shared with NEON-vectorized intersection2by2 #543, NEON-vectorized union2by2 #567 and NEON-vectorized difference #568.
  • 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 intersection2by2 #543 and NEON-vectorized difference #568 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_xor_neon_arm64.s: the kernel.
  • setutil_xor_arm64.go: dispatch and the wrapper that hands the unread tails to the scalar merge.
  • Tests against the scalar merge and an independent bitset oracle: a shared value at every merged lane, every step width and equal maxima, cancellation at each path transition, boundary equality at the copy gates, input shapes (ownership runs, every length pair to 40, value extremes, random), combined length exactly 4096, poison past both inputs, duplicate-input store bounds, and dispatch from the threshold up through the container types.

Testing

go test passes on Graviton 4 with this branch, and passed on Graviton 5 and 2 and on amd64 with the same kernel on 5 September. go fmt clean. Smat fuzzer, 300 seconds on Graviton 5: 594,777 executions, no failures.

Performance

Against master on Graviton 5 (c9g.xlarge, Neoverse V3), medians of five interleaved runs:

BenchmarkXor            665311 ns/op -> 403777 ns/op   (1.65x)
BenchmarkXorLopsided     92851 ns/op ->  92917 ns/op   (1.00x)
BenchmarkXorDense        30185 ns/op ->  29839 ns/op   (1.01x)

Graviton 2 (c6g.large, Neoverse N1): BenchmarkXor 1.41x, the others unchanged. Bulk-merge xor benchmarks within 3 percent either way on both machines.

Kernel against scalar merge at 32 and 2048 elements per side, 128 rotated fixed-seed inputs per cell, worse input order, V3: low-overlap random 1.9x and 7.7x, identical 3.0x and 11.7x, every-eighth-value periodic difference 1.3x and 1.7x. N1: 3.1x and 5.6x, 3.8x and 11.3x, 1.4x and 1.9x.

The threshold of 32 per side is the smallest size at which the kernel beat the scalar merge on every tested shape in both input orders on both cores.

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.
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