| // 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. |
| |
| // Search benchmarks over Int32x4 lanes, across data sizes. Compares three |
| // approaches: |
| // scalar - a plain linear scan of an Int32List. |
| // view - a SIMD scan that views the Int32List as an Int32x4List per call. |
| // direct - a SIMD scan of a list that is already an Int32x4List. |
| // The `view` variant allocates an Int32x4List view on every call, which is |
| // invisible on large arrays but dominates on tiny ones; `direct` avoids it. |
| // |
| // TODO: `direct` is a workaround to measure SIMD search throughput without the |
| // per-call `Int32x4List.view` allocation overhead. Remove it once the `view` |
| // variant is as fast as `direct`. |
| |
| import 'dart:math'; |
| import 'dart:typed_data'; |
| |
| import 'package:benchmark_harness/benchmark_harness.dart'; |
| |
| const _sizes = <String, int>{ |
| '32B': 1 << 3, |
| '64B': 1 << 4, |
| '256B': 1 << 6, |
| '1KiB': 1 << 8, |
| '4KiB': 1 << 10, |
| '64KiB': 1 << 14, |
| '1MiB': 1 << 18, |
| '16MiB': 1 << 22, |
| }; |
| |
| const int _seed = 0x5f3759df; |
| |
| Int32List _randomData(int size) { |
| final rng = Random(_seed); |
| final data = Int32List(size); |
| for (var i = 0; i < size; i++) { |
| data[i] = rng.nextInt(size); |
| } |
| return data; |
| } |
| |
| abstract class _SearchBenchmark extends BenchmarkBase { |
| final int size; |
| int sink = 0; |
| |
| _SearchBenchmark(String variant, this.size) |
| : super('SimdSearch.Int32x4.$variant'); |
| } |
| |
| class _ScalarSearch extends _SearchBenchmark { |
| late Int32List data; |
| |
| _ScalarSearch(String name, int size) : super(name, size); |
| |
| @override |
| void setup() { |
| data = _randomData(size); |
| } |
| |
| @override |
| void run() { |
| // Values are in [0, size), so `size` is absent: every run is a full scan. |
| sink ^= _indexOf(data, size); |
| } |
| |
| static int _indexOf(Int32List data, int needle) { |
| for (var i = 0; i < data.length; i++) { |
| if (data[i] == needle) return i; |
| } |
| return -1; |
| } |
| } |
| |
| class _ViewSearch extends _SearchBenchmark { |
| late Int32List data; |
| |
| _ViewSearch(String name, int size) : super(name, size); |
| |
| @override |
| void setup() { |
| data = _randomData(size); |
| } |
| |
| @override |
| void run() { |
| sink ^= _indexOf(data, size); |
| } |
| |
| // Views the Int32List as an Int32x4List on every call (per-call allocation). |
| static int _indexOf(Int32List data, int needle) { |
| // The SIMD scan covers whole lanes only, so a tail shorter than a lane is |
| // rejected rather than silently skipped. |
| if (data.length % 4 != 0) { |
| throw ArgumentError('data.length must be a multiple of 4'); |
| } |
| final view = Int32x4List.view( |
| data.buffer, |
| data.offsetInBytes, |
| data.length >> 2, |
| ); |
| final needles = Int32x4(needle, needle, needle, needle); |
| for (var i = 0; i < view.length; i++) { |
| final mask = view[i].equal(needles); |
| if (mask.anyTrue) { |
| final base = i << 2; |
| if (mask.flagX) return base; |
| if (mask.flagY) return base + 1; |
| if (mask.flagZ) return base + 2; |
| return base + 3; |
| } |
| } |
| return -1; |
| } |
| } |
| |
| class _DirectSearch extends _SearchBenchmark { |
| late Int32x4List data; |
| |
| _DirectSearch(String name, int size) : super(name, size); |
| |
| @override |
| void setup() { |
| final i32 = _randomData(size); |
| // Built once, so `direct` pays no per-call allocation. |
| data = Int32x4List.view(i32.buffer, 0, size >> 2); |
| } |
| |
| @override |
| void run() { |
| sink ^= _indexOf(data, size); |
| } |
| |
| // Searches a list that is already an Int32x4List (no per-call view). |
| static int _indexOf(Int32x4List data, int needle) { |
| final needles = Int32x4(needle, needle, needle, needle); |
| for (var i = 0; i < data.length; i++) { |
| final mask = data[i].equal(needles); |
| if (mask.anyTrue) { |
| final base = i << 2; |
| if (mask.flagX) return base; |
| if (mask.flagY) return base + 1; |
| if (mask.flagZ) return base + 2; |
| return base + 3; |
| } |
| } |
| return -1; |
| } |
| } |
| |
| void main() { |
| for (final entry in _sizes.entries) { |
| final size = entry.value; |
| _ScalarSearch('${entry.key}.scalar', size).report(); |
| _ViewSearch('${entry.key}.view', size).report(); |
| _DirectSearch('${entry.key}.direct', size).report(); |
| } |
| } |