From ace4bb1ad6ddca0d87edc11862a4533b8b907fc2 Mon Sep 17 00:00:00 2001 From: Modestas Valauskas Date: Mon, 18 May 2026 03:21:30 -0700 Subject: [PATCH] [benchmarks] Add BitArray benchmark Adds a benchmark exercising int.oneBitCount and int.trailingZeroBitCount (landed in CL 498041) and a representative set of Int32x4 SIMD bitwise kernels, through a small bit-array implementation built on a Uint32List word store. Kernels: - cardinality: popcount across the array. - forEachSetBit: Brian-Kernighan iteration via ctz. - select(k): position of the k-th set bit using popcount + ctz. - complementCardinality: popcount of bitwise-NOT per word. - totalBitLength: sum of int.bitLength per word. - intersection / union / xor / difference: pair-op bitwise kernels using Int32x4 SIMD with a scalar tail. - complement: single-input bitwise NOT via Int32x4 XOR-with-all-ones. Each kernel has a Swar baseline using portable bit-twiddle code and an Accelerated variant that uses the new int getters or Int32x4 SIMD, so the speedup attributable to the hardware/intrinsic path is visible directly. main() first runs a correctness check that asserts both implementations agree across a representative range of sizes and bit densities, then reports timings via the standard BenchmarkBase harness. Work towards https://github.com/dart-lang/sdk/issues/1053 (efficient BitSet implementation). Bug: https://github.com/dart-lang/sdk/issues/52673 Change-Id: I5ae5bc0b9d07f6de3e11907ac1fae80f02e77119 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/503020 Commit-Queue: Slava Egorov Reviewed-by: Martin Kustermann Auto-Submit: Modestas Valauskas Reviewed-by: Slava Egorov --- benchmarks/BitArray/dart/BitArray.dart | 700 +++++++++++++++++++++++++ 1 file changed, 700 insertions(+) create mode 100644 benchmarks/BitArray/dart/BitArray.dart diff --git a/benchmarks/BitArray/dart/BitArray.dart b/benchmarks/BitArray/dart/BitArray.dart new file mode 100644 index 00000000000..2e5227067de --- /dev/null +++ b/benchmarks/BitArray/dart/BitArray.dart @@ -0,0 +1,700 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// +// Benchmarks for int.trailingZeroBitCount and int.oneBitCount, exercised +// through a small bit-array implementation. Each operation has an +// `Accelerated` variant (HW intrinsic for counting operations, Int32x4 SIMD +// for word-level bitwise operations) and a `Swar` baseline that does the +// same work without HW acceleration, so the speedup attributable to the +// hardware path is visible directly. +// +// `main()` first runs a correctness check that asserts both +// implementations agree across a range of sizes and densities, then +// reports benchmark timings via the standard BenchmarkBase harness. + +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; + +// 32-bit storage words so the benchmark runs on both native (64-bit int) +// and web (32-bit bitwise) without platform-specific word arithmetic. +const int _wordBits = 32; +const int _wordMask = _wordBits - 1; +const int _wordShift = 5; + +class BitArray { + final Uint32List _words; + final int length; + + BitArray(this.length) + : _words = Uint32List((length + _wordBits - 1) >> _wordShift); + + Uint32List get words => _words; + + void setBit(int i) { + _words[i >> _wordShift] |= 1 << (i & _wordMask); + } + + // ----- cardinality (popcount across all words) --------------------------- + + // Accelerated: hardware popcount via int.oneBitCount. + int cardinalityAccelerated() { + var total = 0; + for (var i = 0; i < _words.length; i++) { + total += _words[i].oneBitCount; + } + return total; + } + + int cardinalitySwar() { + var total = 0; + for (var i = 0; i < _words.length; i++) { + total += _popcountWord(_words[i]); + } + return total; + } + + // ----- forEachSetBit (iterate set bits via ctz + clear-lowest) ----------- + + // Accelerated: int.trailingZeroBitCount per surviving bit. + void forEachSetBitAccelerated(void Function(int) action) { + for (var wordIdx = 0; wordIdx < _words.length; wordIdx++) { + var w = _words[wordIdx]; + final base = wordIdx << _wordShift; + while (w != 0) { + final bit = w.trailingZeroBitCount; + final pos = base + bit; + if (pos >= length) return; + action(pos); + // https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan + w &= w - 1; + } + } + } + + void forEachSetBitSwar(void Function(int) action) { + for (var wordIdx = 0; wordIdx < _words.length; wordIdx++) { + var w = _words[wordIdx]; + final base = wordIdx << _wordShift; + while (w != 0) { + final bit = _ctzWord(w); + final pos = base + bit; + if (pos >= length) return; + action(pos); + // https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan + w &= w - 1; + } + } + } + + // ----- select(k) (position of the k-th set bit, k >= 0) ----------------- + + // Accelerated: int.oneBitCount to skip whole words, then walk the + // target word with int.trailingZeroBitCount. + int selectAccelerated(int k) { + var remaining = k; + for (var wordIdx = 0; wordIdx < _words.length; wordIdx++) { + final w = _words[wordIdx]; + final pop = w.oneBitCount; + if (remaining < pop) { + var v = w; + while (remaining > 0) { + // https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan + v &= v - 1; + remaining--; + } + return (wordIdx << _wordShift) + v.trailingZeroBitCount; + } + remaining -= pop; + } + return -1; + } + + // Fair non-accelerated baseline: SWAR popcount to skip whole words, + // SWAR ctz inside the target word. + int selectSwar(int k) { + var remaining = k; + for (var wordIdx = 0; wordIdx < _words.length; wordIdx++) { + final w = _words[wordIdx]; + final pop = _popcountWord(w); + if (remaining < pop) { + var v = w; + while (remaining > 0) { + // https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan + v &= v - 1; + remaining--; + } + return (wordIdx << _wordShift) + _ctzWord(v); + } + remaining -= pop; + } + return -1; + } + + // ----- totalBitLength (sum of int.bitLength across every word) ---------- + // + // A synthetic operation that exercises int.bitLength once per word, the + // way cardinality exercises int.oneBitCount once per word. + + // Accelerated: int.bitLength (asm-intrinsic CLZ today). + int totalBitLengthAccelerated() { + var total = 0; + for (var i = 0; i < _words.length; i++) { + total += _words[i].bitLength; + } + return total; + } + + // Software bit-trick equivalent of bitLength on a 32-bit word. + int totalBitLengthSwar() { + var total = 0; + for (var i = 0; i < _words.length; i++) { + final v = _words[i]; + if (v != 0) total += _highBitInWord(v) + 1; + } + return total; + } + + // ----- complementCardinality (popcount of bitwise NOT each word) ------- + // + // Exercises int.~ once per word, immediately followed by oneBitCount. + + // Accelerated: int.oneBitCount on each complemented word. + int complementCardinalityAccelerated() { + var total = 0; + for (var i = 0; i < _words.length; i++) { + // Mask back to 32 bits because ~uint32 sign-extends in Dart's int. + total += (~_words[i] & 0xFFFFFFFF).oneBitCount; + } + return total; + } + + // Fair non-accelerated baseline: SWAR popcount on each complemented word. + int complementCardinalitySwar() { + var total = 0; + for (var i = 0; i < _words.length; i++) { + total += _popcountWord(~_words[i] & 0xFFFFFFFF); + } + return total; + } + + // ----- intersection (out = a AND b, materialized as a new BitArray) ----- + + // Word-level scalar AND. + static void intersectionSwar(BitArray a, BitArray b, BitArray out) { + final wa = a._words; + final wb = b._words; + final wo = out._words; + if (wa.length != wb.length || wa.length != wo.length) { + throw ArgumentError('a, b, and out must have the same length'); + } + final n = wa.length; + for (var i = 0; i < n; i++) { + wo[i] = wa[i] & wb[i]; + } + } + + // Accelerated: 4 words per Int32x4 vector op. Tail uses scalar. + static void intersectionAccelerated(BitArray a, BitArray b, BitArray out) { + final wa = a._words; + final wb = b._words; + final wo = out._words; + if (wa.length != wb.length || wa.length != wo.length) { + throw ArgumentError('a, b, and out must have the same length'); + } + final n = wa.length; + final simdWords = n >> 2; + final va = Int32x4List.view(wa.buffer, wa.offsetInBytes, simdWords); + final vb = Int32x4List.view(wb.buffer, wb.offsetInBytes, simdWords); + final vo = Int32x4List.view(wo.buffer, wo.offsetInBytes, simdWords); + for (var i = 0; i < simdWords; i++) { + vo[i] = va[i] & vb[i]; + } + for (var i = simdWords << 2; i < n; i++) { + wo[i] = wa[i] & wb[i]; + } + } + + // ----- union (out = a OR b) ------------------------------------------- + + static void unionSwar(BitArray a, BitArray b, BitArray out) { + final wa = a._words; + final wb = b._words; + final wo = out._words; + if (wa.length != wb.length || wa.length != wo.length) { + throw ArgumentError('a, b, and out must have the same length'); + } + final n = wa.length; + for (var i = 0; i < n; i++) { + wo[i] = wa[i] | wb[i]; + } + } + + static void unionAccelerated(BitArray a, BitArray b, BitArray out) { + final wa = a._words; + final wb = b._words; + final wo = out._words; + if (wa.length != wb.length || wa.length != wo.length) { + throw ArgumentError('a, b, and out must have the same length'); + } + final n = wa.length; + final simdWords = n >> 2; + final va = Int32x4List.view(wa.buffer, wa.offsetInBytes, simdWords); + final vb = Int32x4List.view(wb.buffer, wb.offsetInBytes, simdWords); + final vo = Int32x4List.view(wo.buffer, wo.offsetInBytes, simdWords); + for (var i = 0; i < simdWords; i++) { + vo[i] = va[i] | vb[i]; + } + for (var i = simdWords << 2; i < n; i++) { + wo[i] = wa[i] | wb[i]; + } + } + + // ----- xor (out = a XOR b, symmetric difference) ----------------------- + + static void xorSwar(BitArray a, BitArray b, BitArray out) { + final wa = a._words; + final wb = b._words; + final wo = out._words; + if (wa.length != wb.length || wa.length != wo.length) { + throw ArgumentError('a, b, and out must have the same length'); + } + final n = wa.length; + for (var i = 0; i < n; i++) { + wo[i] = wa[i] ^ wb[i]; + } + } + + static void xorAccelerated(BitArray a, BitArray b, BitArray out) { + final wa = a._words; + final wb = b._words; + final wo = out._words; + if (wa.length != wb.length || wa.length != wo.length) { + throw ArgumentError('a, b, and out must have the same length'); + } + final n = wa.length; + final simdWords = n >> 2; + final va = Int32x4List.view(wa.buffer, wa.offsetInBytes, simdWords); + final vb = Int32x4List.view(wb.buffer, wb.offsetInBytes, simdWords); + final vo = Int32x4List.view(wo.buffer, wo.offsetInBytes, simdWords); + for (var i = 0; i < simdWords; i++) { + vo[i] = va[i] ^ vb[i]; + } + for (var i = simdWords << 2; i < n; i++) { + wo[i] = wa[i] ^ wb[i]; + } + } + + // ----- difference (out = a AND NOT b) ---------------------------------- + // + // Set difference: bits set in `a` and not in `b`. Implemented as + // a & ~b at the word / SIMD level. + + static void differenceSwar(BitArray a, BitArray b, BitArray out) { + final wa = a._words; + final wb = b._words; + final wo = out._words; + if (wa.length != wb.length || wa.length != wo.length) { + throw ArgumentError('a, b, and out must have the same length'); + } + final n = wa.length; + for (var i = 0; i < n; i++) { + wo[i] = wa[i] & (~wb[i] & 0xFFFFFFFF); + } + } + + static void differenceAccelerated(BitArray a, BitArray b, BitArray out) { + final wa = a._words; + final wb = b._words; + final wo = out._words; + if (wa.length != wb.length || wa.length != wo.length) { + throw ArgumentError('a, b, and out must have the same length'); + } + final n = wa.length; + final simdWords = n >> 2; + final va = Int32x4List.view(wa.buffer, wa.offsetInBytes, simdWords); + final vb = Int32x4List.view(wb.buffer, wb.offsetInBytes, simdWords); + final vo = Int32x4List.view(wo.buffer, wo.offsetInBytes, simdWords); + // Int32x4 has no andNot; build via a & ~b (bitwise negate is a single + // NEON EOR-with-all-ones on ARM64). + final ones = Int32x4(-1, -1, -1, -1); + for (var i = 0; i < simdWords; i++) { + vo[i] = va[i] & (vb[i] ^ ones); + } + for (var i = simdWords << 2; i < n; i++) { + wo[i] = wa[i] & (~wb[i] & 0xFFFFFFFF); + } + } + + // ----- complement (out = NOT a) ---------------------------------------- + + static void complementSwar(BitArray a, BitArray out) { + final wa = a._words; + final wo = out._words; + if (wa.length != wo.length) { + throw ArgumentError('a and out must have the same length'); + } + final n = wa.length; + for (var i = 0; i < n; i++) { + wo[i] = ~wa[i] & 0xFFFFFFFF; + } + } + + static void complementAccelerated(BitArray a, BitArray out) { + final wa = a._words; + final wo = out._words; + if (wa.length != wo.length) { + throw ArgumentError('a and out must have the same length'); + } + final n = wa.length; + final simdWords = n >> 2; + final va = Int32x4List.view(wa.buffer, wa.offsetInBytes, simdWords); + final vo = Int32x4List.view(wo.buffer, wo.offsetInBytes, simdWords); + // No SIMD NOT; emit XOR-with-all-ones, which the VM lowers to a single + // NEON EOR on ARM64. + final ones = Int32x4(-1, -1, -1, -1); + for (var i = 0; i < simdWords; i++) { + vo[i] = va[i] ^ ones; + } + for (var i = simdWords << 2; i < n; i++) { + wo[i] = ~wa[i] & 0xFFFFFFFF; + } + } + + bool wordsEqual(BitArray other) { + if (_words.length != other._words.length) return false; + for (var i = 0; i < _words.length; i++) { + if (_words[i] != other._words[i]) return false; + } + return true; + } + + // ----- helpers ---------------------------------------------------------- + + // SWAR popcount on a 32-bit word; non-accelerated equivalent of + // int.oneBitCount. + static int _popcountWord(int v) { + v = v - ((v >> 1) & 0x55555555); + v = (v & 0x33333333) + ((v >> 2) & 0x33333333); + v = (v + (v >> 4)) & 0x0F0F0F0F; + return ((v * 0x01010101) >> 24) & 0xFF; + } + + // SWAR count-trailing-zeros on a 32-bit word; non-accelerated equivalent + // of int.trailingZeroBitCount. Returns 32 for zero. + static int _ctzWord(int v) { + if (v == 0) return _wordBits; + var n = 0; + if ((v & 0xFFFF) == 0) { + n += 16; + v >>= 16; + } + if ((v & 0xFF) == 0) { + n += 8; + v >>= 8; + } + if ((v & 0xF) == 0) { + n += 4; + v >>= 4; + } + if ((v & 0x3) == 0) { + n += 2; + v >>= 2; + } + if ((v & 0x1) == 0) { + n += 1; + } + return n; + } + + // SWAR log2 on a 32-bit word; non-accelerated equivalent of + // int.bitLength minus one (for nonzero v). + static int _highBitInWord(int v) { + var r = 0; + if (v >= 0x10000) { + v >>= 16; + r += 16; + } + if (v >= 0x100) { + v >>= 8; + r += 8; + } + if (v >= 0x10) { + v >>= 4; + r += 4; + } + if (v >= 0x4) { + v >>= 2; + r += 2; + } + if (v >= 0x2) { + r += 1; + } + return r; + } +} + +// ----- Correctness check ---------------------------------------------------- + +BitArray _randomArray(int size, Random rng, int densityPercent) { + final bits = BitArray(size); + for (var i = 0; i < size; i++) { + if (rng.nextInt(100) < densityPercent) bits.setBit(i); + } + return bits; +} + +void _assertEq(Object? actual, Object? expected, String label) { + if (actual != expected) { + throw StateError('FAIL $label: expected $expected, got $actual'); + } +} + +void checkCorrectness() { + final rng = Random(0xDA27); + for (final size in const [0, 1, 31, 32, 33, 63, 64, 65, 127, 1000, 50000]) { + for (final density in const [0, 3, 50, 97, 100]) { + final bits = _randomArray(size, rng, density); + + // Cardinality operations must agree. SWAR is the oracle. + final card = bits.cardinalitySwar(); + _assertEq( + bits.cardinalityAccelerated(), + card, + 'cardinality.accelerated(size=$size, density=$density)', + ); + + // forEachSetBit: accelerated and swar must enumerate the same + // bits in the same order. + final acc = []; + final swar = []; + bits.forEachSetBitAccelerated(acc.add); + bits.forEachSetBitSwar(swar.add); + _assertEq( + acc.length, + swar.length, + 'forEach count (size=$size, density=$density)', + ); + for (var i = 0; i < acc.length; i++) { + _assertEq( + acc[i], + swar[i], + 'forEach[$i] (size=$size, density=$density)', + ); + } + + // select(k): accelerated and swar must agree. + for (final k in [0, 1, card >> 1, card - 1]) { + if (k < 0 || k >= card) continue; + _assertEq( + bits.selectAccelerated(k), + bits.selectSwar(k), + 'select(size=$size, density=$density, k=$k)', + ); + } + + _assertEq( + bits.complementCardinalityAccelerated(), + bits.complementCardinalitySwar(), + 'complementCardinality(size=$size, density=$density)', + ); + + _assertEq( + bits.totalBitLengthAccelerated(), + bits.totalBitLengthSwar(), + 'totalBitLength(size=$size, density=$density)', + ); + + // Bitwise pair ops: scalar and SIMD must produce identical results. + final other = _randomArray(size, rng, density); + final label = 'size=$size, density=$density'; + + final iSwar = BitArray(size); + final iAcc = BitArray(size); + BitArray.intersectionSwar(bits, other, iSwar); + BitArray.intersectionAccelerated(bits, other, iAcc); + _assertEq(iAcc.wordsEqual(iSwar), true, 'intersection($label)'); + + final uSwar = BitArray(size); + final uAcc = BitArray(size); + BitArray.unionSwar(bits, other, uSwar); + BitArray.unionAccelerated(bits, other, uAcc); + _assertEq(uAcc.wordsEqual(uSwar), true, 'union($label)'); + + final xSwar = BitArray(size); + final xAcc = BitArray(size); + BitArray.xorSwar(bits, other, xSwar); + BitArray.xorAccelerated(bits, other, xAcc); + _assertEq(xAcc.wordsEqual(xSwar), true, 'xor($label)'); + + final dSwar = BitArray(size); + final dAcc = BitArray(size); + BitArray.differenceSwar(bits, other, dSwar); + BitArray.differenceAccelerated(bits, other, dAcc); + _assertEq(dAcc.wordsEqual(dSwar), true, 'difference($label)'); + + // Complement: scalar and SIMD must produce the same bit-flipped array. + final cSwar = BitArray(size); + final cAcc = BitArray(size); + BitArray.complementSwar(bits, cSwar); + BitArray.complementAccelerated(bits, cAcc); + _assertEq(cAcc.wordsEqual(cSwar), true, 'complement($label)'); + } + } +} + +// ----- Benchmarks ----------------------------------------------------------- + +const int _benchSize = 1 << 20; // 1,048,576 bits = 32,768 words. + +class _BitArrayBenchmark extends BenchmarkBase { + final int densityPercent; + final void Function(BitArray) operation; + late BitArray bits; + + _BitArrayBenchmark(String name, this.densityPercent, this.operation) + : super('BitArray.$name'); + + @override + void setup() { + bits = _randomArray(_benchSize, Random(0xBEEF), densityPercent); + } + + @override + void run() { + operation(bits); + } +} + +class _BitArrayPairBenchmark extends BenchmarkBase { + final int densityPercent; + final void Function(BitArray, BitArray, BitArray) operation; + late BitArray a; + late BitArray b; + late BitArray out; + + _BitArrayPairBenchmark(String name, this.densityPercent, this.operation) + : super('BitArray.$name'); + + @override + void setup() { + a = _randomArray(_benchSize, Random(0xBEEF), densityPercent); + b = _randomArray(_benchSize, Random(0xC0DE), densityPercent); + out = BitArray(_benchSize); + } + + @override + void run() { + operation(a, b, out); + } +} + +List _benchmarks() { + // Sinks the optimizer cannot fold away. + var sink = 0; + void accumulate(int x) { + sink ^= x; + } + + return [ + // Cardinality: SWAR popcount vs int.oneBitCount. + _BitArrayBenchmark( + 'cardinality.swar', + 25, + (bits) => sink ^= bits.cardinalitySwar(), + ), + _BitArrayBenchmark( + 'cardinality.accelerated', + 25, + (bits) => sink ^= bits.cardinalityAccelerated(), + ), + + // forEachSetBit: Brian-Kernighan loop with SWAR ctz vs int.trailingZeroBitCount. + _BitArrayBenchmark( + 'forEachSetBit.swar', + 25, + (bits) => bits.forEachSetBitSwar(accumulate), + ), + _BitArrayBenchmark( + 'forEachSetBit.accelerated', + 25, + (bits) => bits.forEachSetBitAccelerated(accumulate), + ), + + // select(k) for k near the middle of a quarter-full array. + _BitArrayBenchmark( + 'select.swar', + 25, + (bits) => sink ^= bits.selectSwar(_benchSize >> 3), + ), + _BitArrayBenchmark( + 'select.accelerated', + 25, + (bits) => sink ^= bits.selectAccelerated(_benchSize >> 3), + ), + + // complementCardinality: popcount of (~w & 0xFFFFFFFF) per word. + _BitArrayBenchmark( + 'complementCardinality.swar', + 25, + (bits) => sink ^= bits.complementCardinalitySwar(), + ), + _BitArrayBenchmark( + 'complementCardinality.accelerated', + 25, + (bits) => sink ^= bits.complementCardinalityAccelerated(), + ), + + // totalBitLength: sum int.bitLength across every word. + _BitArrayBenchmark( + 'totalBitLength.swar', + 25, + (bits) => sink ^= bits.totalBitLengthSwar(), + ), + _BitArrayBenchmark( + 'totalBitLength.accelerated', + 25, + (bits) => sink ^= bits.totalBitLengthAccelerated(), + ), + + // Bitwise pair ops: scalar word-level vs Int32x4 SIMD. + _BitArrayPairBenchmark('intersection.swar', 25, BitArray.intersectionSwar), + _BitArrayPairBenchmark( + 'intersection.accelerated', + 25, + BitArray.intersectionAccelerated, + ), + _BitArrayPairBenchmark('union.swar', 25, BitArray.unionSwar), + _BitArrayPairBenchmark('union.accelerated', 25, BitArray.unionAccelerated), + _BitArrayPairBenchmark('xor.swar', 25, BitArray.xorSwar), + _BitArrayPairBenchmark('xor.accelerated', 25, BitArray.xorAccelerated), + _BitArrayPairBenchmark('difference.swar', 25, BitArray.differenceSwar), + _BitArrayPairBenchmark( + 'difference.accelerated', + 25, + BitArray.differenceAccelerated, + ), + + // Complement: scalar ~ vs Int32x4 XOR-with-all-ones. Ignores the + // second input of the pair-benchmark harness. + _BitArrayPairBenchmark( + 'complement.swar', + 25, + (a, _, out) => BitArray.complementSwar(a, out), + ), + _BitArrayPairBenchmark( + 'complement.accelerated', + 25, + (a, _, out) => BitArray.complementAccelerated(a, out), + ), + ]; +} + +void main() { + checkCorrectness(); + for (final benchmark in _benchmarks()) { + benchmark.report(); + } +}