| // Copyright (c) 2021, 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. |
| |
| import 'dart:collection'; |
| |
| import 'package:benchmark_harness/benchmark_harness.dart'; |
| |
| // Benchmark for polymorphic map copying. |
| // |
| // The set of benchmarks compares the cost of copying default Maps |
| // (LinkedHashMaps) and HashMaps, for small and large maps. |
| // |
| // The maps have a key type `Object?`, since we want to use Strings, ints and |
| // user-defined types. The class `Thing` is a used-defined type with an |
| // inexpensive hashCode operation. String keys are interesting because they are |
| // quite common, and are special-cased in the JavaScript runtime. |
| // |
| // Benchmarks have names following this pattern: |
| // |
| // MapCopy.{Map,HashMap}.{String,Thing}.of.{Map,HashMap}.{N} |
| // MapCopy.{Map,HashMap}.{String,Thing}.copyOf.{Map,HashMap}.{N} |
| // MapCopy.{Map,HashMap}.{String,Thing}.fromEntries.{Map,HashMap}.{N} |
| // |
| // For example, MapCopy.Map.String.of.HashMap.2 would call |
| // |
| // Map<Object, Object>.of(m) |
| // |
| // where `m` is a `HashMap<String, Object>` with 2 entries. |
| // |
| // The `copyOf` variant creates an empty map and populates it using `forEach`, |
| // so MapCopy.HashMap.Thing.copyOf.HashMap.100 would call: |
| // |
| // HashMap<Object, Object> result = HashMap(); |
| // m.forEach((key, value) { result[key] = value; }); |
| // |
| // where `m` is a `HashMap<Thing, Object>` with 100 entries. |
| // |
| // The `fromEntries` variant creates a map via `Map.fromEntries(other.entries)`. |
| // |
| // Benchmarks are run for small maps (e.g. 2 entries, names ending in `.2`) and |
| // 'large' maps (100 entries or `.100`). The benchmarks are normalized on the |
| // number of elements to make benchmarks with different input sizes more |
| // comparable. |
| |
| abstract class Benchmark<K> extends BenchmarkBase { |
| final String targetKind; // 'Map' or 'HashMap'. |
| late final String keyKind = _keyKind(K); // 'String' or 'Thing' or 'int'. |
| final String methodKind; // 'of' or 'copyOf' or 'fromEntries'. |
| final String sourceKind; // 'Map' or 'HashMap'. |
| final int length; |
| final List<Map<Object?, Object>> inputs = []; |
| |
| Benchmark(this.targetKind, this.methodKind, this.sourceKind, this.length) |
| : super('MapCopy.$targetKind.${_keyKind(K)}.$methodKind.$sourceKind' |
| '.$length'); |
| |
| static String _keyKind(Type type) { |
| if (type == String) return 'String'; |
| if (type == int) return 'int'; |
| if (type == Thing) return 'Thing'; |
| throw UnsupportedError('Unsupported type $type'); |
| } |
| |
| /// Override this method with one that will copy [input] to [output]. |
| void copy(); |
| |
| @override |
| void setup() { |
| // Ensure setup() is idempotent. |
| if (inputs.isNotEmpty) return; |
| |
| const totalEntries = 1000; |
| |
| int totalLength = 0; |
| while (totalLength < totalEntries) { |
| final sample = makeSample(); |
| inputs.add(sample); |
| totalLength += sample.length; |
| } |
| |
| // Sanity checks. |
| for (var sample in inputs) { |
| if (sample.length != length) throw 'Wrong length: $length $sample'; |
| } |
| if (totalLength != totalEntries) { |
| throw 'totalLength $totalLength != expected $totalEntries'; |
| } |
| } |
| |
| int _sequence = 0; |
| |
| Map<Object?, Object> makeSample() { |
| late final Map<K, Object> sample; |
| if (sourceKind == 'Map') sample = {}; |
| if (sourceKind == 'HashMap') sample = HashMap(); |
| for (int i = 1; i <= length; i++) { |
| _sequence = (_sequence + 119) & 0x1ffffff; |
| final K key = makeKey(_sequence); |
| sample[key] = i; |
| } |
| return sample; |
| } |
| |
| K makeKey(int i) { |
| if (keyKind == 'String') return 'key-$i' as K; |
| if (keyKind == 'int') return i as K; |
| if (keyKind == 'Thing') return Thing() as K; |
| throw UnsupportedError('Unsupported type $K'); |
| } |
| |
| @override |
| void run() { |
| for (var sample in inputs) { |
| input = sample; |
| copy(); |
| } |
| if (output.length != inputs.first.length) throw 'Bad result: $output'; |
| } |
| } |
| |
| class Thing { |
| static int _counter = 0; |
| final int _index; |
| Thing() : _index = ++_counter; |
| |
| @override |
| bool operator ==(Object other) => other is Thing && _index == other._index; |
| |
| @override |
| int get hashCode => _index; |
| } |
| |
| // All the 'copy' methods use [input] and [output] rather than a parameter and |
| // return value to avoid the possibility of a parametric covariance type check |
| // in the call sequence. |
| Map<Object?, Object> input = {}; |
| var output; |
| |
| class BaselineBenchmark extends Benchmark<String> { |
| BaselineBenchmark(int length) : super('Map', 'baseline', 'Map', length); |
| |
| @override |
| void copy() { |
| // Dummy 'copy' to measure overhead of benchmarking loops. |
| output = input; |
| } |
| } |
| |
| class MapOfBenchmark<K> extends Benchmark<K> { |
| MapOfBenchmark(String sourceKind, int length) |
| : super('Map', 'of', sourceKind, length); |
| |
| @override |
| void copy() { |
| output = Map<Object?, Object>.of(input); |
| } |
| } |
| |
| class HashMapOfBenchmark<K> extends Benchmark<K> { |
| HashMapOfBenchmark(String sourceKind, int length) |
| : super('HashMap', 'of', sourceKind, length); |
| |
| @override |
| void copy() { |
| output = HashMap<Object?, Object>.of(input); |
| } |
| } |
| |
| class MapCopyOfBenchmark<K> extends Benchmark<K> { |
| MapCopyOfBenchmark(String sourceKind, int length) |
| : super('Map', 'copyOf', sourceKind, length); |
| |
| @override |
| void copy() { |
| final map = <Object?, Object>{}; |
| input.forEach((k, v) { |
| map[k] = v; |
| }); |
| output = map; |
| } |
| } |
| |
| class HashMapCopyOfBenchmark<K> extends Benchmark<K> { |
| HashMapCopyOfBenchmark(String sourceKind, int length) |
| : super('HashMap', 'copyOf', sourceKind, length); |
| |
| @override |
| void copy() { |
| final map = HashMap<Object?, Object>(); |
| input.forEach((k, v) { |
| map[k] = v; |
| }); |
| output = map; |
| } |
| } |
| |
| class MapFromEntriesBenchmark<K> extends Benchmark<K> { |
| MapFromEntriesBenchmark(String sourceKind, int length) |
| : super('Map', 'fromEntries', sourceKind, length); |
| |
| @override |
| void copy() { |
| output = Map<Object?, Object>.fromEntries(input.entries); |
| } |
| } |
| |
| class HashMapFromEntriesBenchmark<K> extends Benchmark<K> { |
| HashMapFromEntriesBenchmark(String sourceKind, int length) |
| : super('HashMap', 'fromEntries', sourceKind, length); |
| |
| @override |
| void copy() { |
| output = HashMap<Object?, Object>.fromEntries(input.entries); |
| } |
| } |
| |
| /// Use the common methods for many different kinds of Map to make the calls in |
| /// the runtime implementation polymorphic. |
| void pollute() { |
| final Map<String, Object> m1 = Map.of({'hello': 66}); |
| final Map<String, Object> m2 = HashMap.of(m1); |
| final Map<int, Object> m3 = Map.of({1: 66}); |
| final Map<int, Object> m4 = HashMap.of({1: 66}); |
| final Map<Object, Object> m5 = Map.identity() |
| ..[Thing()] = 1 |
| ..[Thing()] = 2; |
| final Map<Object, Object> m6 = HashMap.identity() |
| ..[Thing()] = 1 |
| ..[Thing()] = 2; |
| final Map<Object, Object> m7 = UnmodifiableMapView(m1); |
| final Map<Object, Object> m8 = UnmodifiableMapView(m2); |
| final Map<Object, Object> m9 = UnmodifiableMapView(m3); |
| final Map<Object, Object> m10 = UnmodifiableMapView(m4); |
| |
| int c = 0; |
| for (final m in [m1, m2, m3, m4, m5, m6, m7, m8, m9, m10]) { |
| final Map<Object, Object> d1 = Map.of(m); |
| final Map<Object, Object> d2 = HashMap.of(m); |
| // ignore: prefer_collection_literals |
| final Map<Object, Object> d3 = Map()..addAll(m); |
| final Map<Object, Object> d4 = {...m, ...m}; |
| final Map<Object, Object> d5 = HashMap()..addAll(m); |
| final Map<Object, Object> d6 = Map.identity()..addAll(m); |
| final Map<Object, Object> d7 = HashMap.identity()..addAll(m); |
| final Map<Object, Object> d8 = Map.fromEntries(m.entries); |
| final Map<Object, Object> d9 = HashMap.fromEntries(m.entries); |
| for (final z in [d1, d2, d3, d4, d5, d6, d7, d8, d9]) { |
| z.forEach((k, v) { |
| c++; |
| }); |
| } |
| } |
| const totalElements = 108; |
| if (c != totalElements) throw StateError('c: $c != $totalElements'); |
| } |
| |
| /// Command-line arguments: |
| /// |
| /// `--baseline`: Run additional benchmarks to measure the benchmarking loop |
| /// component. |
| /// |
| /// `--cross`: Run additional benchmarks for copying between Map and |
| /// HashMap. |
| /// |
| /// `--int`: Run additional benchmarks with `int` keys. |
| /// |
| /// `--1`: Run additional benchmarks for singleton maps. |
| /// |
| /// `--all`: Run all benchmark variants. |
| void main(List<String> commandLineArguments) { |
| final arguments = [...commandLineArguments]; |
| |
| bool includeBaseline = false; |
| final Set<String> kinds = {'same'}; |
| final Set<String> types = {'String', 'Thing'}; |
| final Set<int> sizes = {2, 100}; |
| |
| if (arguments.remove('--reset')) { |
| kinds.clear(); |
| types.clear(); |
| sizes.clear(); |
| } |
| |
| if (arguments.remove('--baseline')) includeBaseline = true; |
| if (arguments.remove('--cross')) kinds.add('cross'); |
| |
| if (arguments.remove('--string')) types.add('String'); |
| if (arguments.remove('--thing')) types.add('Thing'); |
| if (arguments.remove('--int')) types.add('int'); |
| |
| 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')) { |
| kinds.addAll(['baseline', 'same', 'cross']); |
| types.addAll(['String', 'Thing', 'int']); |
| sizes.addAll([1, 2, 100]); |
| } |
| |
| if (arguments.isNotEmpty) { |
| throw ArgumentError('Unused command line arguments: $arguments'); |
| } |
| |
| if (kinds.isEmpty) kinds.add('same'); |
| if (types.isEmpty) types.add('String'); |
| if (sizes.isEmpty) sizes.add(2); |
| |
| List<Benchmark> makeBenchmarks<K>(int length) { |
| return [ |
| // Map from Map |
| if (kinds.contains('same')) ...[ |
| MapOfBenchmark<K>('Map', length), |
| MapCopyOfBenchmark<K>('Map', length), |
| MapFromEntriesBenchmark<K>('Map', length), |
| ], |
| // Map from HashMap |
| if (kinds.contains('cross')) ...[ |
| MapOfBenchmark<K>('HashMap', length), |
| MapCopyOfBenchmark<K>('HashMap', length), |
| MapFromEntriesBenchmark<K>('HashMap', length), |
| ], |
| // HashMap from HashMap |
| if (kinds.contains('same')) ...[ |
| HashMapOfBenchmark<K>('HashMap', length), |
| HashMapCopyOfBenchmark<K>('HashMap', length), |
| HashMapFromEntriesBenchmark<K>('HashMap', length), |
| ], |
| // HashMap from Map |
| if (kinds.contains('cross')) ...[ |
| HashMapOfBenchmark<K>('Map', length), |
| HashMapCopyOfBenchmark<K>('Map', length), |
| HashMapFromEntriesBenchmark<K>('Map', length), |
| ], |
| ]; |
| } |
| |
| List<Benchmark> makeBenchmarksForLength(int length) { |
| return [ |
| if (includeBaseline) BaselineBenchmark(length), |
| if (types.contains('String')) ...makeBenchmarks<String>(length), |
| if (types.contains('Thing')) ...makeBenchmarks<Thing>(length), |
| if (types.contains('int')) ...makeBenchmarks<int>(length), |
| ]; |
| } |
| |
| final benchmarks = [ |
| for (final length in sizes) ...makeBenchmarksForLength(length), |
| ]; |
| |
| // Warmup all benchmarks to ensure JIT compilers see full polymorphism. |
| for (var benchmark in benchmarks) { |
| pollute(); |
| benchmark.setup(); |
| } |
| |
| for (var benchmark in benchmarks) { |
| pollute(); |
| benchmark.warmup(); |
| } |
| |
| for (var benchmark in benchmarks) { |
| // `report` calls `setup`, but `setup` is idempotent. |
| benchmark.report(); |
| } |
| } |