diff --git a/benchmarks/MapCopy/dart/MapCopy.dart b/benchmarks/MapCopy/dart/MapCopy.dart new file mode 100644 index 00000000000..2009c80b636 --- /dev/null +++ b/benchmarks/MapCopy/dart/MapCopy.dart @@ -0,0 +1,367 @@ +// 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.of(m) +// +// where `m` is a `HashMap` 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 result = HashMap(); +// m.forEach((key, value) { result[key] = value; }); +// +// where `m` is a `HashMap` 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 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> 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 makeSample() { + late final Map 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 input = {}; +var output; + +class BaselineBenchmark extends Benchmark { + BaselineBenchmark(int length) : super('Map', 'baseline', 'Map', length); + + @override + void copy() { + // Dummy 'copy' to measure overhead of benchmarking loops. + output = input; + } +} + +class MapOfBenchmark extends Benchmark { + MapOfBenchmark(String sourceKind, int length) + : super('Map', 'of', sourceKind, length); + + @override + void copy() { + output = Map.of(input); + } +} + +class HashMapOfBenchmark extends Benchmark { + HashMapOfBenchmark(String sourceKind, int length) + : super('HashMap', 'of', sourceKind, length); + + @override + void copy() { + output = HashMap.of(input); + } +} + +class MapCopyOfBenchmark extends Benchmark { + MapCopyOfBenchmark(String sourceKind, int length) + : super('Map', 'copyOf', sourceKind, length); + + @override + void copy() { + final map = {}; + input.forEach((k, v) { + map[k] = v; + }); + output = map; + } +} + +class HashMapCopyOfBenchmark extends Benchmark { + HashMapCopyOfBenchmark(String sourceKind, int length) + : super('HashMap', 'copyOf', sourceKind, length); + + @override + void copy() { + final map = HashMap(); + input.forEach((k, v) { + map[k] = v; + }); + output = map; + } +} + +class MapFromEntriesBenchmark extends Benchmark { + MapFromEntriesBenchmark(String sourceKind, int length) + : super('Map', 'fromEntries', sourceKind, length); + + @override + void copy() { + output = Map.fromEntries(input.entries); + } +} + +class HashMapFromEntriesBenchmark extends Benchmark { + HashMapFromEntriesBenchmark(String sourceKind, int length) + : super('HashMap', 'fromEntries', sourceKind, length); + + @override + void copy() { + output = HashMap.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 m1 = Map.of({'hello': 66}); + final Map m2 = HashMap.of(m1); + final Map m3 = Map.of({1: 66}); + final Map m4 = HashMap.of({1: 66}); + final Map m5 = Map.identity() + ..[Thing()] = 1 + ..[Thing()] = 2; + final Map m6 = HashMap.identity() + ..[Thing()] = 1 + ..[Thing()] = 2; + final Map m7 = UnmodifiableMapView(m1); + final Map m8 = UnmodifiableMapView(m2); + final Map m9 = UnmodifiableMapView(m3); + final Map m10 = UnmodifiableMapView(m4); + + int c = 0; + for (final m in [m1, m2, m3, m4, m5, m6, m7, m8, m9, m10]) { + final Map d1 = Map.of(m); + final Map d2 = HashMap.of(m); + // ignore: prefer_collection_literals + final Map d3 = Map()..addAll(m); + final Map d4 = {...m, ...m}; + final Map d5 = HashMap()..addAll(m); + final Map d6 = Map.identity()..addAll(m); + final Map d7 = HashMap.identity()..addAll(m); + final Map d8 = Map.fromEntries(m.entries); + final Map 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 commandLineArguments) { + final arguments = [...commandLineArguments]; + + bool includeBaseline = false; + final Set kinds = {'same'}; + final Set types = {'String', 'Thing'}; + final Set 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 makeBenchmarks(int length) { + return [ + // Map from Map + if (kinds.contains('same')) ...[ + MapOfBenchmark('Map', length), + MapCopyOfBenchmark('Map', length), + MapFromEntriesBenchmark('Map', length), + ], + // Map from HashMap + if (kinds.contains('cross')) ...[ + MapOfBenchmark('HashMap', length), + MapCopyOfBenchmark('HashMap', length), + MapFromEntriesBenchmark('HashMap', length), + ], + // HashMap from HashMap + if (kinds.contains('same')) ...[ + HashMapOfBenchmark('HashMap', length), + HashMapCopyOfBenchmark('HashMap', length), + HashMapFromEntriesBenchmark('HashMap', length), + ], + // HashMap from Map + if (kinds.contains('cross')) ...[ + HashMapOfBenchmark('Map', length), + HashMapCopyOfBenchmark('Map', length), + HashMapFromEntriesBenchmark('Map', length), + ], + ]; + } + + List makeBenchmarksForLength(int length) { + return [ + if (includeBaseline) BaselineBenchmark(length), + if (types.contains('String')) ...makeBenchmarks(length), + if (types.contains('Thing')) ...makeBenchmarks(length), + if (types.contains('int')) ...makeBenchmarks(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(); + } +} diff --git a/benchmarks/MapCopy/dart2/MapCopy.dart b/benchmarks/MapCopy/dart2/MapCopy.dart new file mode 100644 index 00000000000..3f7c000eff2 --- /dev/null +++ b/benchmarks/MapCopy/dart2/MapCopy.dart @@ -0,0 +1,11 @@ +// 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. + +// @dart=2.9 + +import '../dart/MapCopy.dart' as benchmark; + +void main(List arguments) { + benchmark.main(arguments); +}