blob: 738319f142b62d6caab01d3f5ee2c48c4ff9698b [file] [log] [blame] [edit]
// Copyright (c) 2025, 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.
// Invariance optimizations benchmark based on adding and removing from a copy
// of the default implementation of Queue.
//
// This benchmark incurs type checks due to parametric subtype covariance. Many
// of these type checks can be optimized away by invariance analysis to discover
// which types are invariant.
//
// Some compilers already optimize '`this`-calls`, where the receiver is
// `this`. `this`-calls are a trivial kind of invariance since the caller and
// callee share the same receiver, so share the same class type variables. An
// example of this is when `ListQueue.add` calls `ListQueue._add`. There is no
// need to check `value as E` again.
//
// One example that is not well optimized at the time of writing is when type
// variables of related collections are invariant. The Queue implementation
// uses, as a backing store, a fixed length list that has a type invariant with
// respect to the `Queue<E>`, constructed via `List<E?>.filled(...)`.
//
// There are two axes of benchmark variation:
// 1. The queue element type (`int`, `int?`, `List<int>`, records)
// 2. Adding elements via `Queue.add` and `Queue.addAll`.
//
// The first axis exercises a variety of covariance checks. More complex checks
// tend to be slower, magnifying the benefit of invariance analysis.
//
// The second axis, using `add` and `addAll`, adds more opportunities for
// invariances to be detected (`_queue` and `_items` are separate collections
// that have the same value of the type variable).
//
// All benchmarks use the workload defined in `Benchmark<T>` so that the
// compilers don't achieve optimizations by flowing constant types. We avoid
// mixins because some implementations expand mixins, which could result in the
// expanded code having non-parametric types, e.g. `Queue<int>`.
import 'dart:typed_data';
import 'package:benchmark_harness/benchmark_harness.dart';
abstract class Benchmark<T> extends BenchmarkBase {
final int _iterations;
final Queue<T> _queue = Queue<T>();
final bool _runAddAll;
Benchmark(String name, String type, this._iterations)
: _runAddAll = name.startsWith('addAll'),
super('InvarianceQueue.$name.$type');
// For the add/remove benchmarks, values are copied through this field. `T` is
// invariant with respect to the Queue element type.
T? _item;
// For the addAll/remove benchmarks, values are copied through this second
// Queue which contains one element at the time of copy.
final Queue<T> _items = Queue<T>();
@override
void run() {
if (_runAddAll) {
runAddAllRemove();
} else {
runAddRemove();
}
}
void check() {
if (_runAddAll) {
checkAddAllRemove();
} else {
checkAddRemove();
}
}
void runAddRemove() {
for (int i = 0; i < _iterations; i++) {
_item = _queue.removeFirst();
_queue.add(_item as T);
}
}
void checkAddRemove() {
if (_item == _queue.first) throw StateError('bad');
}
void runAddAllRemove() {
for (int i = 0; i < _iterations; i++) {
_items
..clear()
..add(_queue.removeFirst());
_queue.addAll(_items);
}
}
void checkAddAllRemove() {
if (_items.single == _queue.first) throw StateError('bad');
}
}
abstract class QueueInt extends Benchmark<int> {
QueueInt(String name, int count) : super(name, 'int', count);
@override
void setup() {
_queue
..clear()
..add(1)
..add(2)
..add(3)
..add(4);
}
}
abstract class QueueIntQ extends Benchmark<int?> {
QueueIntQ(String name, int count) : super(name, 'intQ', count);
@override
void setup() {
_queue
..clear()
..add(1)
..add(2)
..add(3)
..add(null);
}
}
abstract class QueueListInt extends Benchmark<List<int>> {
QueueListInt(String name, int count) : super(name, 'List.int', count);
@override
void setup() {
_queue
..clear()
..add('1'.codeUnits)
..add([2])
..add(Uint8List(3))
..add(Int16List(4));
}
}
typedef SimpleRecord = (int, int);
typedef ComplexRecord = (num, num Function(num), {Comparable z});
abstract class QueueSimpleRecord extends Benchmark<SimpleRecord> {
QueueSimpleRecord(String name, int count)
: super(name, 'simple-record', count);
@override
void setup() {
_queue
..clear()
..add((0, 1))
..add((1, 2))
..add((2, 3))
..add((3, 0));
}
}
abstract class QueueComplexRecord extends Benchmark<ComplexRecord> {
QueueComplexRecord(String name, int count)
: super(name, 'complex-record', count);
@override
void setup() {
_queue
..clear()
..add((1.2, 1.2.compareTo, z: 'x'))
..add((0, (x) => x + 1, z: 123))
// Type depends on class type variable:
..add((1, Env<num>().foo, z: BigInt.parse('42')))
// Type depends on function type variable:
..add((1, Env<num>().bar<int>(), z: DateTime.now()));
}
}
class Env<T> {
T foo(T x) => x;
S Function(T) bar<S extends T>() {
S localFunction(T x) {
if (x is S) return x;
throw 'bad';
}
return localFunction;
}
}
class QueueAddRemoveInt extends QueueInt {
QueueAddRemoveInt(int count) : super('add-remove', count);
}
class QueueAddRemoveIntQ extends QueueIntQ {
QueueAddRemoveIntQ(int count) : super('add-remove', count);
}
class QueueAddRemoveListInt extends QueueListInt {
QueueAddRemoveListInt(int count) : super('add-remove', count);
}
class QueueAddRemoveSimpleRecord extends QueueSimpleRecord {
QueueAddRemoveSimpleRecord(int count) : super('add-remove', count);
}
class QueueAddRemoveComplexRecord extends QueueComplexRecord {
QueueAddRemoveComplexRecord(int count) : super('add-remove', count);
}
class QueueAddAllRemoveInt extends QueueInt {
QueueAddAllRemoveInt(int count) : super('addAll-remove', count);
}
class QueueAddAllRemoveIntQ extends QueueIntQ {
QueueAddAllRemoveIntQ(int count) : super('addAll-remove', count);
}
class QueueAddAllRemoveListInt extends QueueListInt {
QueueAddAllRemoveListInt(int count) : super('addAll-remove', count);
}
class QueueAddAllRemoveSimpleRecord extends QueueSimpleRecord {
QueueAddAllRemoveSimpleRecord(int count) : super('addAll-remove', count);
}
class QueueAddAllRemoveComplexRecord extends QueueComplexRecord {
QueueAddAllRemoveComplexRecord(int count) : super('addAll-remove', count);
}
void pollute() {
// Pollute run size.
QueueAddRemoveInt(0)
..setup()
..run();
QueueAddRemoveIntQ(1)
..setup()
..run();
QueueAddRemoveListInt(2)
..setup()
..run();
// Further pollute type.
Queue<String>()
..add('a')
..add('b')
..addAll({'a', 'b'})
..addAll(['a', 'b'])
..toString();
Queue<Pattern>()
..add('a')
..add(RegExp('b'))
..toString();
Queue<String Function(String)>()..add(Env<String>().foo);
Queue<num>()
..addAll(<int>[1])
..addAll(<double>[2.3]);
}
void main() {
final benchmarks = <Benchmark>[
QueueAddRemoveInt(1000),
QueueAddRemoveIntQ(1000),
QueueAddRemoveListInt(1000),
QueueAddRemoveSimpleRecord(1000),
QueueAddRemoveComplexRecord(1000),
QueueAddAllRemoveInt(1000),
QueueAddAllRemoveIntQ(1000),
QueueAddAllRemoveListInt(1000),
QueueAddAllRemoveSimpleRecord(1000),
QueueAddAllRemoveComplexRecord(1000),
];
// Warmup all benchmarks to ensure JIT compilers see full polymorphism.
for (final benchmark in benchmarks) {
pollute();
benchmark.setup();
}
for (final benchmark in benchmarks) {
pollute();
benchmark.warmup();
}
for (final benchmark in benchmarks) {
// `report` calls `setup`, but `setup` is idempotent.
benchmark.report();
benchmark.check();
}
}
/// What follows is a copy of the SDK Queue implementation, cut down to remove
/// methods that are unused in this benchmark.
/// A [Queue] is a collection that can be manipulated at both ends.
abstract interface class Queue<E> implements Iterable<E> {
/// Creates a queue.
factory Queue() = ListQueue<E>;
/// Removes and returns the first element of this queue.
E removeFirst();
/// Removes and returns the last element of the queue.
E removeLast();
/// Adds [value] at the beginning of the queue.
void addFirst(E value);
/// Adds [value] at the end of the queue.
void addLast(E value);
/// Adds [value] at the end of the queue.
void add(E value);
/// Removes a single instance of [value] from the queue.
///
/// Returns `true` if a value was removed, or `false` if the queue
/// contained no element equal to [value].
bool remove(Object? value);
/// Adds all elements of [iterable] at the end of the queue. The
/// length of the queue is extended by the length of [iterable].
void addAll(Iterable<E> iterable);
/// Removes all elements in the queue. The size of the queue becomes zero.
void clear();
}
/// List based [Queue].
final class ListQueue<E> implements Queue<E> {
static const int _INITIAL_CAPACITY = 8;
List<E?> _table;
int _head;
int _tail;
int _modificationCount = 0;
/// Create an empty queue.
///
/// If [initialCapacity] is given, prepare the queue for at least that many
/// elements.
ListQueue([int? initialCapacity])
: _head = 0,
_tail = 0,
_table = List<E?>.filled(_calculateCapacity(initialCapacity), null);
static int _calculateCapacity(int? initialCapacity) {
if (initialCapacity == null || initialCapacity < _INITIAL_CAPACITY) {
return _INITIAL_CAPACITY;
} else if (!_isPowerOf2(initialCapacity)) {
return _nextPowerOf2(initialCapacity);
}
assert(_isPowerOf2(initialCapacity));
return initialCapacity;
}
// Iterable interface.
@override
Queue<R> cast<R>() => throw UnimplementedError('Iterable.cast');
@override
Iterator<E> get iterator => _ListQueueIterator<E>(this);
@override
void forEach(void Function(E element) f) {
int modificationCount = _modificationCount;
for (int i = _head; i != _tail; i = (i + 1) & (_table.length - 1)) {
f(_table[i] as E);
_checkModification(modificationCount);
}
}
@override
bool get isEmpty => _head == _tail;
@override
bool get isNotEmpty => !isEmpty;
@override
int get length => (_tail - _head) & (_table.length - 1);
@override
E get first {
if (_head == _tail) throw IterableElementError.noElement();
return _table[_head] as E;
}
@override
E get last {
if (_head == _tail) throw IterableElementError.noElement();
return _table[(_tail - 1) & (_table.length - 1)] as E;
}
@override
E get single {
if (_head == _tail) throw IterableElementError.noElement();
if (length > 1) throw IterableElementError.tooMany();
return _table[_head] as E;
}
@override
E elementAt(int index) {
IndexError.check(index, length, indexable: this);
return _table[(_head + index) & (_table.length - 1)] as E;
}
@override
List<E> toList({bool growable = true}) {
int mask = _table.length - 1;
int length = (_tail - _head) & mask;
if (length == 0) return List<E>.empty(growable: growable);
var list = List<E>.filled(length, first, growable: growable);
for (int i = 0; i < length; i++) {
list[i] = _table[(_head + i) & mask] as E;
}
return list;
}
// Collection interface.
@override
void add(E value) {
_add(value);
}
@override
void addAll(Iterable<E> elements) {
if (elements is List<E>) {
List<E> list = elements;
int addCount = list.length;
int length = this.length;
if (length + addCount >= _table.length) {
_preGrow(length + addCount);
// After preGrow, all elements are at the start of the list.
_table.setRange(length, length + addCount, list, 0);
_tail += addCount;
} else {
// Adding addCount elements won't reach _head.
int endSpace = _table.length - _tail;
if (addCount < endSpace) {
_table.setRange(_tail, _tail + addCount, list, 0);
_tail += addCount;
} else {
int preSpace = addCount - endSpace;
_table.setRange(_tail, _tail + endSpace, list, 0);
_table.setRange(0, preSpace, list, endSpace);
_tail = preSpace;
}
}
_modificationCount++;
} else {
for (E element in elements) {
_add(element);
}
}
}
@override
bool remove(Object? value) {
for (int i = _head; i != _tail; i = (i + 1) & (_table.length - 1)) {
E? element = _table[i];
if (element == value) {
_remove(i);
_modificationCount++;
return true;
}
}
return false;
}
@override
void clear() {
if (_head != _tail) {
for (int i = _head; i != _tail; i = (i + 1) & (_table.length - 1)) {
_table[i] = null;
}
_head = _tail = 0;
_modificationCount++;
}
}
@override
String toString() => Iterable.iterableToFullString(this, '{', '}');
// Queue interface.
@override
void addLast(E value) {
_add(value);
}
@override
void addFirst(E value) {
_head = (_head - 1) & (_table.length - 1);
_table[_head] = value;
if (_head == _tail) _grow();
_modificationCount++;
}
@override
E removeFirst() {
if (_head == _tail) throw IterableElementError.noElement();
_modificationCount++;
E result = _table[_head] as E;
_table[_head] = null;
_head = (_head + 1) & (_table.length - 1);
return result;
}
@override
E removeLast() {
if (_head == _tail) throw IterableElementError.noElement();
_modificationCount++;
_tail = (_tail - 1) & (_table.length - 1);
E result = _table[_tail] as E;
_table[_tail] = null;
return result;
}
// Internal helper functions.
/// Whether [number] is a power of two.
///
/// Only works for positive numbers.
static bool _isPowerOf2(int number) => (number & (number - 1)) == 0;
/// Rounds [number] up to the nearest power of 2.
///
/// If [number] is a power of 2 already, it is returned.
///
/// Only works for positive numbers.
static int _nextPowerOf2(int number) {
assert(number > 0);
number = (number << 1) - 1;
for (;;) {
int nextNumber = number & (number - 1);
if (nextNumber == 0) return number;
number = nextNumber;
}
}
/// Check if the queue has been modified during iteration.
void _checkModification(int expectedModificationCount) {
if (expectedModificationCount != _modificationCount) {
throw ConcurrentModificationError(this);
}
}
/// Adds element at end of queue. Used by both [add] and [addAll].
void _add(E element) {
_table[_tail] = element;
_tail = (_tail + 1) & (_table.length - 1);
if (_head == _tail) _grow();
_modificationCount++;
}
/// Removes the element at [offset] into [_table].
///
/// Removal is performed by linearly moving elements either before or after
/// [offset] by one position.
///
/// Returns the new offset of the following element. This may be the same
/// offset or the following offset depending on how elements are moved
/// to fill the hole.
int _remove(int offset) {
int mask = _table.length - 1;
int startDistance = (offset - _head) & mask;
int endDistance = (_tail - offset) & mask;
if (startDistance < endDistance) {
// Closest to start.
int i = offset;
while (i != _head) {
int prevOffset = (i - 1) & mask;
_table[i] = _table[prevOffset];
i = prevOffset;
}
_table[_head] = null;
_head = (_head + 1) & mask;
return (offset + 1) & mask;
} else {
_tail = (_tail - 1) & mask;
int i = offset;
while (i != _tail) {
int nextOffset = (i + 1) & mask;
_table[i] = _table[nextOffset];
i = nextOffset;
}
_table[_tail] = null;
return offset;
}
}
/// Grow the table when full.
void _grow() {
List<E?> newTable = List<E?>.filled(_table.length * 2, null);
int split = _table.length - _head;
newTable.setRange(0, split, _table, _head);
newTable.setRange(split, split + _head, _table, 0);
_head = 0;
_tail = _table.length;
_table = newTable;
}
int _writeToList(List<E?> target) {
assert(target.length >= length);
if (_head <= _tail) {
int length = _tail - _head;
target.setRange(0, length, _table, _head);
return length;
} else {
int firstPartSize = _table.length - _head;
target.setRange(0, firstPartSize, _table, _head);
target.setRange(firstPartSize, firstPartSize + _tail, _table, 0);
return _tail + firstPartSize;
}
}
/// Grows the table even if it is not full.
void _preGrow(int newElementCount) {
assert(newElementCount >= length);
// Add some extra room to ensure that there's room for more elements after
// expansion.
newElementCount += newElementCount >> 1;
int newCapacity = _nextPowerOf2(newElementCount);
List<E?> newTable = List<E?>.filled(newCapacity, null);
_tail = _writeToList(newTable);
_table = newTable;
_head = 0;
}
/// Unimplemented Iterable methods
@override
Never any(_) => throw UnimplementedError();
@override
Never contains(_) => throw UnimplementedError();
@override
Never every(_) => throw UnimplementedError();
@override
Never expand<T>(_) => throw UnimplementedError();
@override
Never firstWhere(_, {E Function()? orElse}) => throw UnimplementedError();
@override
Never fold<T>(_, _) => throw UnimplementedError();
@override
Never followedBy(_) => throw UnimplementedError();
@override
Never join([String _ = '']) => throw UnimplementedError();
@override
Never lastWhere(_, {E Function()? orElse}) => throw UnimplementedError();
@override
Never map<T>(_) => throw UnimplementedError();
@override
Never reduce(_) => throw UnimplementedError();
@override
Never singleWhere(_, {E Function()? orElse}) => throw UnimplementedError();
@override
Never skip(_) => throw UnimplementedError();
@override
Never skipWhile(_) => throw UnimplementedError();
@override
Never take(_) => throw UnimplementedError();
@override
Never takeWhile(_) => throw UnimplementedError();
@override
Never toSet() => throw UnimplementedError();
@override
Never where(_) => throw UnimplementedError();
@override
Never whereType<T>() => throw UnimplementedError();
}
/// Iterator for a [ListQueue].
///
/// Considers any add or remove operation a concurrent modification.
class _ListQueueIterator<E> implements Iterator<E> {
final ListQueue<E> _queue;
final int _end;
final int _modificationCount;
int _position;
E? _current;
_ListQueueIterator(ListQueue<E> queue)
: _queue = queue,
_end = queue._tail,
_modificationCount = queue._modificationCount,
_position = queue._head;
@override
E get current => _current as E;
@override
bool moveNext() {
_queue._checkModification(_modificationCount);
if (_position == _end) {
_current = null;
return false;
}
_current = _queue._table[_position];
_position = (_position + 1) & (_queue._table.length - 1);
return true;
}
}
abstract class IterableElementError {
static StateError noElement() => StateError('No element');
static StateError tooMany() => StateError('Too many elements');
}