dart-sdk/benchmarks/Iterators/dart/Iterators.dart
Jonas Termansen b6aa2976dc [benchmarks] Fix benchmarks warming up incorrectly.
The benchmarks were using a range of antipatterns that did not do what
the authors thought they did. It seems that the authors thought the
warmup method has to run for a while and do the full warmup, but the
truth is that the harness will do that for you by running the warmup
function in a timed loop. Instead these patterns just wasted time by
making the warmup more expensive and complex than needed.

This change just removes the warmup overrides since none of them do
anything positive. This change prepares us for future improvements to
the benchmark harness.

Fixes: b/324874055
Change-Id: Ib7c282123a2151614bc95a105a30e67221f11315
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/352022
Reviewed-by: William Hesse <whesse@google.com>
Commit-Queue: Jonas Termansen <sortie@google.com>
2024-02-22 14:38:19 +00:00

653 lines
19 KiB
Dart

// 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();
}
}
}