mirror of
https://github.com/dart-lang/sdk
synced 2024-07-01 07:14:29 +00:00
[benchmark] Add MapCopy benchmark
Change-Id: I8684dc9c79f9194999f89de1587a72067ec72426 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/225880 Reviewed-by: Jonas Termansen <sortie@google.com> Commit-Queue: Stephen Adams <sra@google.com>
This commit is contained in:
parent
2f222990dc
commit
d8c8474387
367
benchmarks/MapCopy/dart/MapCopy.dart
Normal file
367
benchmarks/MapCopy/dart/MapCopy.dart
Normal file
|
@ -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<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();
|
||||
}
|
||||
}
|
11
benchmarks/MapCopy/dart2/MapCopy.dart
Normal file
11
benchmarks/MapCopy/dart2/MapCopy.dart
Normal file
|
@ -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<String> arguments) {
|
||||
benchmark.main(arguments);
|
||||
}
|
Loading…
Reference in New Issue
Block a user