| // Copyright (c) 2022, 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. |
| |
| /// # Benchmark for iterators of common collections. |
| /// |
| /// The purpose of this benchmark is to detect performance changes in the |
| /// iterators for common collections (system Lists, Maps, etc). |
| /// |
| /// ## Polymorphic benchmarks |
| /// |
| /// Benchmark names beginning with `Iterators.poly.`. |
| /// |
| /// These benchmark use the iterators from a common polymorphic for-in loop, so |
| /// none of the methods involved in iterating are inlined. This gives an |
| /// indication of worst-case performance. |
| /// |
| /// Iterables of different sizes (small (N=1) and large (N=100)) are used to |
| /// give insight into the fixed vs per-element costs. |
| /// |
| /// Results are normalized by iterating 1000 elements and reporting the time per |
| /// element. There is an outer loop that calls the iterator loop in `sinkAll`. |
| /// |
| /// The dispatched (polymorphic) calls are to `get:iterator`, `moveNext` and |
| /// `get:current`. |
| /// |
| /// | N | outer | `get:iterator` | `moveNext` | `get:current` | |
| /// | ---: | ----: | -------------: | ---------: | ------------: | |
| /// | 0* | 1000 | 1000 | 1000 | 0 | |
| /// | 1 | 1000 | 1000 | 2000 | 1000 | |
| /// | 2* | 500 | 500 | 1500 | 1000 | |
| /// | 100 | 10 | 10 | 1010 | 1000 | |
| /// |
| /// * By default only the N=1 and N=100 benchmarks arer run. The N=0 and N=2 |
| /// series are available running manually with `--0` and `--2` command-line |
| /// arguments. |
| /// |
| /// Generic Iterables have benchmarks for different element types. There are |
| /// benchmarks for `int` type arguments, which have a fast type test, and for |
| /// `Thing<Iterable<Comparable>>`, which is harder to test quickly. These tests |
| /// are distinguished by `int` and `Hard` in the name. |
| /// |
| /// ## Monomorphic benchmarks |
| /// |
| /// Benchmark names beginning with `Iterators.mono.`. |
| /// |
| /// A subset of the polymorphic benchmarks are also implemented with a |
| /// per-benchmark for-in loop directly iterating a collection of known |
| /// representation. This gives the compiler the opportunity to inline the |
| /// methods into the loop and represents the best-case performance. |
| /// |
| /// ## Example benchmarks |
| /// |
| /// The name has 4-7 words separated by periods. The first word is always |
| /// 'Iterators', and the second is either 'mono' for monomorphic loops, or |
| /// 'poly' for benchmarks using a shared polymorphic loop. The last word is a |
| /// number which is the size (length) of the Iterable. |
| /// |
| /// ### Iterators.mono.const.Map.int.values.100 |
| /// |
| /// A for-in loop over the values iterable of a known constant Map with value |
| /// type `int` and 100 entries. |
| /// |
| /// ### Iterators.poly.Runes.1 |
| /// |
| /// An iteration over the String.runes iterable of a single character String |
| /// using the shared polymorphic loop. |
| /// |
| /// ### Iterators.poly.HashMap.Hard.keys.100 |
| /// |
| /// An iteration of over the keys iterable of a HashMap with key type |
| /// `Thing<Iterable<Comparable>>` and 100 entries. |
| /// |
| /// ### Iterators.*.UpTo.* |
| /// |
| /// The UpTo iterable is a minimal iterable that provides successive |
| /// numbers. The `moveNext` and `get:current` methods are small. Comparing |
| /// Iterators.poly.UpTo.*.100 to Iterators.poly.*.100 gives an indication of how |
| /// much work is done by `moveNext` (and sometimes `get:current`). |
| /// |
| /// ### Iterators.mono.Nothing.* |
| /// |
| /// The Nothing benchmark has no iteration over an iterable and is used to get a |
| /// baseline time for running the benchmark loop for monomorphic |
| /// benchmarks. This can be a substantial fraction of |
| /// |
| /// Consider the times |
| /// |
| /// Iterators.mono.CodeUnits.1 = 7.0ns |
| /// Iterators.mono.Nothing.1 = 3.1ns |
| /// |
| /// Because the trip count (i.e. 1) of the for-in loop is so small, there is a |
| /// lot of overhead attributable to the outer loop in `MonoBenchmark.run`. The |
| /// 1000/1 = 1000 trips of outer loops takes 3.1us (3.1ns * 1000 trips), so |
| /// CodeUnits is spending only 7.0-3.1 = 3.9ns per character in the for-in |
| /// loop over the `.codeUnits` of the single-character String. |
| /// |
| /// Iterators.mono.CodeUnits.100 = 1.83ns |
| /// Iterators.mono.Nothing.100 = 0.05ns |
| /// |
| /// Now the outer loop runs only 1000/100 = 10 times, for 0.05us. If we subtract |
| /// this from 1.83, we get 1.78ns per character for long strings. |
| /// |
| library iterators_benchmark; |
| |
| // TODO(48277): Update when fixed: |
| // ignore_for_file: unnecessary_lambdas |
| |
| import 'dart:collection'; |
| |
| import 'package:benchmark_harness/benchmark_harness.dart'; |
| |
| import 'data.dart'; |
| |
| const targetSize = 1000; |
| |
| class Emitter implements ScoreEmitter { |
| @override |
| void emit(String testName, double value) { |
| // [value] is microseconds per ten calls to `run()`. |
| final nanoSeconds = value * 1000; |
| final singleElementTimeNs = nanoSeconds / 10 / targetSize; |
| print('$testName(RunTimeRaw): $singleElementTimeNs ns.'); |
| } |
| } |
| |
| abstract class Benchmark extends BenchmarkBase { |
| final int size; |
| bool selected = false; |
| Benchmark._(String name, this.size) |
| : super('Iterators.$name.$size', emitter: Emitter()); |
| |
| factory Benchmark(String name, int size, Iterable Function(int) generate) = |
| PolyBenchmark; |
| } |
| |
| abstract class MonoBenchmark extends Benchmark { |
| final int _repeats; |
| MonoBenchmark(String name, int size) |
| : _repeats = size == 0 ? targetSize : targetSize ~/ size, |
| super._('mono.$name', size); |
| |
| @override |
| void run() { |
| for (int i = 0; i < _repeats; i++) { |
| sinkMono(); |
| } |
| } |
| |
| void sinkMono(); |
| } |
| |
| class PolyBenchmark extends Benchmark { |
| final Iterable Function(int) generate; |
| final List<Iterable> inputs = []; |
| |
| PolyBenchmark(String name, int size, this.generate) |
| : super._('poly.$name', size); |
| |
| @override |
| void setup() { |
| if (inputs.isNotEmpty) return; // Ensure setup() is idempotent. |
| |
| int totalSize = 0; |
| while (totalSize < targetSize) { |
| final sample = generate(size); |
| inputs.add(sample); |
| totalSize += size == 0 ? 1 : size; |
| } |
| } |
| |
| @override |
| void run() { |
| for (int i = 0; i < inputs.length; i++) { |
| sinkAll(inputs[i]); |
| } |
| } |
| } |
| |
| /// This function is the inner loop of the benchmark. |
| @pragma('dart2js:noInline') |
| @pragma('vm:never-inline') |
| @pragma('wasm:never-inline') |
| void sinkAll(Iterable iterable) { |
| for (final value in iterable) { |
| sink = value; |
| } |
| } |
| |
| Object? sink; |
| |
| class BenchmarkConstMapIntKeys1 extends MonoBenchmark { |
| BenchmarkConstMapIntKeys1() : super('const.Map.int.keys', 1); |
| |
| static const _map = constMapIntInt1; |
| |
| @override |
| void sinkMono() { |
| for (final value in _map.keys) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkConstMapIntKeys2 extends MonoBenchmark { |
| BenchmarkConstMapIntKeys2() : super('const.Map.int.keys', 2); |
| |
| static const _map = constMapIntInt2; |
| |
| @override |
| void sinkMono() { |
| for (final value in _map.keys) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkConstMapIntKeys100 extends MonoBenchmark { |
| BenchmarkConstMapIntKeys100() : super('const.Map.int.keys', 100); |
| |
| static const _map = constMapIntInt100; |
| |
| @override |
| void sinkMono() { |
| for (final value in _map.keys) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkConstMapIntValues1 extends MonoBenchmark { |
| BenchmarkConstMapIntValues1() : super('const.Map.int.values', 1); |
| |
| static const _map = constMapIntInt1; |
| |
| @override |
| void sinkMono() { |
| for (final value in _map.values) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkConstMapIntValues2 extends MonoBenchmark { |
| BenchmarkConstMapIntValues2() : super('const.Map.int.values', 2); |
| |
| static const _map = constMapIntInt2; |
| |
| @override |
| void sinkMono() { |
| for (final value in _map.values) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkConstMapIntValues100 extends MonoBenchmark { |
| BenchmarkConstMapIntValues100() : super('const.Map.int.values', 100); |
| |
| static const _map = constMapIntInt100; |
| |
| @override |
| void sinkMono() { |
| for (final value in _map.values) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkMapIntKeys extends MonoBenchmark { |
| BenchmarkMapIntKeys(int size) : super('Map.int.keys', size) { |
| _map.addAll(generateMapIntInt(size)); |
| } |
| |
| final Map<int, int> _map = {}; |
| |
| @override |
| void sinkMono() { |
| for (final value in _map.keys) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkUpTo extends MonoBenchmark { |
| BenchmarkUpTo(int size) : super('UpTo', size); |
| |
| @override |
| void sinkMono() { |
| for (final value in UpTo(size)) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkNothing extends MonoBenchmark { |
| BenchmarkNothing(int size) : super('Nothing', size); |
| |
| @override |
| void sinkMono() { |
| sink = size; |
| } |
| } |
| |
| class BenchmarkCodeUnits extends MonoBenchmark { |
| BenchmarkCodeUnits(int size) |
| : string = generateString(size), |
| super('CodeUnits', size); |
| |
| final String string; |
| |
| @override |
| void sinkMono() { |
| for (final value in string.codeUnits) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkListIntGrowable extends MonoBenchmark { |
| BenchmarkListIntGrowable(int size) |
| : _list = List.generate(size, (i) => i), |
| super('List.int.growable', size); |
| |
| final List<int> _list; |
| |
| @override |
| void sinkMono() { |
| for (final value in _list) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkListIntSystem1 extends MonoBenchmark { |
| // The List type here is not quite monomorphic. It is the choice between two |
| // 'system' Lists: a const List and a growable List. It is quite common to |
| // have growable and const lists at the same use-site (e.g. the const coming |
| // from a default argument). |
| // |
| // Ideally some combination of the class hierarchy or compiler tricks would |
| // ensure there is little cost of having this gentle polymorphism. |
| BenchmarkListIntSystem1(int size) |
| : _list1 = List.generate(size, (i) => i), |
| _list2 = generateConstListOfInt(size), |
| super('List.int.growable.and.const', size); |
| |
| final List<int> _list1; |
| final List<int> _list2; |
| bool _flip = false; |
| |
| @override |
| void sinkMono() { |
| _flip = !_flip; |
| final list = _flip ? _list1 : _list2; |
| for (final value in list) { |
| sink = value; |
| } |
| } |
| } |
| |
| class BenchmarkListIntSystem2 extends MonoBenchmark { |
| // The List type here is not quite monomorphic. It is the choice between two |
| // 'system' Lists: a const List and a fixed-length List. It is quite common to |
| // have fixed-length and const lists at the same use-site (e.g. the const |
| // coming from a default argument). |
| // |
| // Ideally some combination of the class hierarchy or compiler tricks would |
| // ensure there is little cost of having this gentle polymorphism. |
| BenchmarkListIntSystem2(int size) |
| : _list1 = List.generate(size, (i) => i, growable: false), |
| _list2 = generateConstListOfInt(size), |
| super('List.int.fixed.and.const', size); |
| |
| final List<int> _list1; |
| final List<int> _list2; |
| bool _flip = false; |
| |
| @override |
| void sinkMono() { |
| _flip = !_flip; |
| final list = _flip ? _list1 : _list2; |
| for (final value in list) { |
| sink = value; |
| } |
| } |
| } |
| |
| /// A simple Iterable that yields the integers 0 through `length`. |
| /// |
| /// This Iterable serves as the minimal interesting example to serve as a |
| /// baseline, and is useful in constructing other benchmark inputs. |
| class UpTo extends IterableBase<int> { |
| final int _length; |
| UpTo(this._length); |
| |
| @override |
| Iterator<int> get iterator => UpToIterator(_length); |
| } |
| |
| class UpToIterator implements Iterator<int> { |
| final int _length; |
| int _position = 0; |
| int? _current; |
| |
| UpToIterator(this._length); |
| |
| @override |
| int get current => _current!; |
| |
| @override |
| bool moveNext() { |
| if (_position < _length) { |
| _current = _position++; |
| return true; |
| } |
| _current = null; |
| return false; |
| } |
| } |
| |
| /// A `Thing` has a type parameter which makes type tests in the Iterators |
| /// potentially harder, and equality uses the type parameter, making Iterables |
| /// that do lookups slower. |
| class Thing<T> { |
| static int _nextIndex = 0; |
| final int _index; |
| Thing() : _index = _nextIndex++; |
| |
| @override |
| int get hashCode => _index; |
| |
| @override |
| bool operator ==(Object other) => other is Thing<T> && other._index == _index; |
| } |
| |
| final thingGenerators = [ |
| // TODO(48277): Use instantiated constructor tear-offs when fixed: |
| () => Thing<Set<String>>(), |
| () => Thing<Set<Duration>>(), |
| () => Thing<Set<BigInt>>(), |
| () => Thing<Queue<String>>(), |
| () => Thing<Queue<Duration>>(), |
| () => Thing<Queue<BigInt>>(), |
| () => Thing<List<String>>(), |
| () => Thing<List<Duration>>(), |
| () => Thing<List<BigInt>>(), |
| ]; |
| |
| int _generateThingListState = 0; |
| List<Thing<Iterable<Comparable>>> generateThingList(int n) { |
| Thing nextThing(_) { |
| final next = (_generateThingListState++).remainder(thingGenerators.length); |
| return thingGenerators[next](); |
| } |
| |
| return List.from(UpTo(n).map(nextThing)); |
| } |
| |
| Map<Thing<Iterable<Comparable>>, Thing<Iterable<Comparable>>> generateThingMap( |
| int n) { |
| return Map.fromIterables(generateThingList(n), generateThingList(n)); |
| } |
| |
| Map<Thing<Iterable<Comparable>>, Thing<Iterable<Comparable>>> |
| generateThingHashMap(int n) { |
| return HashMap.fromIterables(generateThingList(n), generateThingList(n)); |
| } |
| |
| int _generateStringState = 0; |
| String generateString(int n) { |
| return ((_generateStringState++).isEven ? 'x' : '\u2192') * n; |
| } |
| |
| Map<int, int> generateMapIntInt(int n) => |
| Map<int, int>.fromIterables(UpTo(n), UpTo(n)); |
| |
| Map<int, int> generateIdentityMapIntInt(int n) { |
| return Map<int, int>.identity()..addAll(generateMapIntInt(n)); |
| } |
| |
| /// Run the benchmark loop on various inputs to pollute type inference and JIT |
| /// caches. |
| void pollute() { |
| // This iterable reads `sink` mid-loop, making it infeasible for the compiler |
| // to move the write to `sink` out of the loop. |
| sinkAll(UpTo(100).map((i) { |
| if (i > 0 && sink != i - 1) throw StateError('sink'); |
| return i; |
| })); |
| |
| // TODO(sra): Do we need to add anything here? There are a lot of benchmarks, |
| // so that is probably sufficient to make the necessary places polymorphic. |
| } |
| |
| /// Command-line arguments: |
| /// |
| /// `--0`: Run benchmarks for empty iterables. |
| /// `--1`: Run benchmarks for singleton iterables. |
| /// `--2`: Run benchmarks for two-element iterables. |
| /// `--100`: Run benchmarks for 100-element iterables. |
| /// |
| /// Default sizes are 1 and 100. |
| /// |
| /// `--all`: Run all benchmark variants and sizes. |
| /// |
| /// `foo`, `foo.bar`: a Selector. |
| /// |
| /// Run benchmarks with name containing all the dot-separated words in the |
| /// selector, so `--Set.const` will run benchmark |
| /// `Iterators.const.Set.int.N`, and `--2.UpTo` will select |
| /// `Iterators.UpTo.2`. Each selector is matched independently, and if |
| /// selectors are used, only benchmarks matching some selector are run. |
| /// |
| void main(List<String> commandLineArguments) { |
| final arguments = [...commandLineArguments]; |
| |
| const allSizes = {0, 1, 2, 100}; |
| const defaultSizes = {1, 100}; |
| final allSizeWords = Set.unmodifiable(allSizes.map((size) => '$size')); |
| |
| final Set<int> sizes = {}; |
| final Set<String> selectors = {}; |
| |
| if (arguments.remove('--0')) sizes.add(0); |
| if (arguments.remove('--1')) sizes.add(1); |
| if (arguments.remove('--2')) sizes.add(2); |
| if (arguments.remove('--100')) sizes.add(100); |
| |
| if (arguments.remove('--all')) { |
| sizes.addAll(allSizes); |
| } |
| |
| selectors.addAll(arguments); |
| |
| if (sizes.isEmpty) sizes.addAll(defaultSizes); |
| if (selectors.isEmpty) selectors.add('Iterators'); |
| |
| List<Benchmark> makeBenchmarksForSize(int size) { |
| return [ |
| // Simple |
| BenchmarkNothing(size), |
| BenchmarkUpTo(size), |
| BenchmarkCodeUnits(size), |
| Benchmark('UpTo', size, (n) => UpTo(n)), |
| Benchmark('CodeUnits', size, (n) => generateString(n).codeUnits), |
| Benchmark('Runes', size, (n) => generateString(n).runes), |
| // --- |
| BenchmarkListIntGrowable(size), |
| BenchmarkListIntSystem1(size), |
| BenchmarkListIntSystem2(size), |
| Benchmark('List.int.growable', size, |
| (n) => List<int>.of(UpTo(n), growable: true)), |
| Benchmark('List.int.fixed', size, |
| (n) => List<int>.of(UpTo(n), growable: false)), |
| Benchmark('List.int.unmodifiable', size, |
| (n) => List<int>.unmodifiable(UpTo(n))), |
| // --- |
| Benchmark('List.Hard.growable', size, generateThingList), |
| // --- |
| Benchmark('Set.int', size, (n) => Set<int>.of(UpTo(n))), |
| Benchmark('const.Set.int', size, generateConstSetOfInt), |
| // --- |
| BenchmarkMapIntKeys(size), |
| Benchmark('Map.int.keys', size, (n) => generateMapIntInt(n).keys), |
| Benchmark('Map.int.values', size, (n) => generateMapIntInt(n).values), |
| Benchmark('Map.int.entries', size, (n) => generateMapIntInt(n).entries), |
| // --- |
| Benchmark('Map.identity.int.keys', size, |
| (n) => generateIdentityMapIntInt(n).keys), |
| Benchmark('Map.identity.int.values', size, |
| (n) => generateIdentityMapIntInt(n).values), |
| Benchmark('Map.identity.int.entries', size, |
| (n) => generateIdentityMapIntInt(n).entries), |
| // --- |
| Benchmark( |
| 'const.Map.int.keys', size, (n) => generateConstMapIntInt(n).keys), |
| Benchmark('const.Map.int.values', size, |
| (n) => generateConstMapIntInt(n).values), |
| Benchmark('const.Map.int.entries', size, |
| (n) => generateConstMapIntInt(n).entries), |
| // --- |
| Benchmark('Map.Hard.keys', size, (n) => generateThingMap(n).keys), |
| Benchmark('Map.Hard.values', size, (n) => generateThingMap(n).values), |
| // --- |
| Benchmark('HashMap.int.keys', size, |
| (n) => HashMap<int, int>.fromIterables(UpTo(n), UpTo(n)).keys), |
| Benchmark('HashMap.int.values', size, |
| (n) => HashMap<int, int>.fromIterables(UpTo(n), UpTo(n)).values), |
| Benchmark('HashMap.int.entries', size, |
| (n) => HashMap<int, int>.fromIterables(UpTo(n), UpTo(n)).entries), |
| // --- |
| Benchmark('HashMap.Hard.keys', size, (n) => generateThingHashMap(n).keys), |
| Benchmark( |
| 'HashMap.Hard.values', size, (n) => generateThingHashMap(n).values), |
| ]; |
| } |
| |
| final benchmarks = [ |
| BenchmarkConstMapIntKeys1(), |
| BenchmarkConstMapIntKeys2(), |
| BenchmarkConstMapIntKeys100(), |
| BenchmarkConstMapIntValues1(), |
| BenchmarkConstMapIntValues2(), |
| BenchmarkConstMapIntValues100(), |
| for (final size in allSizes) ...makeBenchmarksForSize(size), |
| ]; |
| |
| // Select benchmarks |
| final unusedSelectors = {...selectors}; |
| for (final benchmark in benchmarks) { |
| final nameWords = benchmark.name.split('.').toSet(); |
| for (final selector in selectors) { |
| final selectorWords = selector.split('.').toSet(); |
| if (nameWords.containsAll(selectorWords)) { |
| unusedSelectors.remove(selector); |
| if (selectorWords.any(allSizeWords.contains) || |
| sizes.contains(benchmark.size)) { |
| benchmark.selected = true; |
| } |
| // continue matching to remove other matching selectors. |
| } |
| } |
| } |
| if (unusedSelectors.isNotEmpty) { |
| throw ArgumentError(unusedSelectors, 'selectors match no benchmark'); |
| } |
| |
| // Warmup all benchmarks to ensure JIT compilers see full polymorphism. |
| for (var benchmark in benchmarks) { |
| pollute(); |
| benchmark.setup(); |
| } |
| |
| // Warm up all the benchmarks, including the non-selected ones. |
| for (int i = 0; i < 10; i++) { |
| for (var benchmark in benchmarks) { |
| pollute(); |
| final marker = Object(); |
| sink = marker; |
| benchmark.warmup(); |
| if (benchmark.size > 0 && identical(sink, marker)) throw 'unexpected'; |
| } |
| } |
| |
| for (var benchmark in benchmarks) { |
| // `report` calls `setup`, but `setup` is idempotent. |
| if (benchmark.selected) { |
| benchmark.report(); |
| } |
| } |
| } |