// 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>`, 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>` 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; 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 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 _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 _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 _list1; final List _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 _list1; final List _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 { final int _length; UpTo(this._length); @override Iterator get iterator => UpToIterator(_length); } class UpToIterator implements Iterator { 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 { static int _nextIndex = 0; final int _index; Thing() : _index = _nextIndex++; @override int get hashCode => _index; @override bool operator ==(Object other) => other is Thing && other._index == _index; } final thingGenerators = [ Thing>.new, Thing>.new, Thing>.new, Thing>.new, Thing>.new, Thing>.new, Thing>.new, Thing>.new, Thing>.new, ]; int _generateThingListState = 0; List>> generateThingList(int n) { Thing nextThing(_) { final next = (_generateThingListState++).remainder(thingGenerators.length); return thingGenerators[next](); } return List.from(UpTo(n).map(nextThing)); } Map>, Thing>> generateThingMap( int n, ) { return Map.fromIterables(generateThingList(n), generateThingList(n)); } Map>, Thing>> generateThingHashMap(int n) { return HashMap.fromIterables(generateThingList(n), generateThingList(n)); } int _generateStringState = 0; String generateString(int n) { return ((_generateStringState++).isEven ? 'x' : '\u2192') * n; } Map generateMapIntInt(int n) => Map.fromIterables(UpTo(n), UpTo(n)); Map generateIdentityMapIntInt(int n) { return Map.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 commandLineArguments) { final arguments = [...commandLineArguments]; const allSizes = {0, 1, 2, 100}; const defaultSizes = {1, 100}; final allSizeWords = Set.unmodifiable(allSizes.map((size) => '$size')); final Set sizes = {}; final Set 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 makeBenchmarksForSize(int size) { return [ // Simple BenchmarkNothing(size), BenchmarkUpTo(size), BenchmarkCodeUnits(size), Benchmark('UpTo', size, UpTo.new), 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.of(UpTo(n), growable: true), ), Benchmark( 'List.int.fixed', size, (n) => List.of(UpTo(n), growable: false), ), Benchmark( 'List.int.unmodifiable', size, (n) => List.unmodifiable(UpTo(n)), ), // --- Benchmark('List.Hard.growable', size, generateThingList), // --- Benchmark('Set.int', size, (n) => Set.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.fromIterables(UpTo(n), UpTo(n)).keys, ), Benchmark( 'HashMap.int.values', size, (n) => HashMap.fromIterables(UpTo(n), UpTo(n)).values, ), Benchmark( 'HashMap.int.entries', size, (n) => HashMap.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(); } } }