| // Copyright (c) 2023, 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. | |
| // | |
| // Generates both the dart and dart2 version of this benchmark. | |
| import 'dart:io'; | |
| import 'dart:math'; | |
| import 'package:path/path.dart' as path; | |
| const String benchmarkName = 'SubtypeTestCache'; | |
| const List<int> assertionCounts = [ | |
| 1, | |
| 5, | |
| 10, | |
| 25, | |
| 50, | |
| 75, | |
| 100, | |
| 250, | |
| 500, | |
| 750, | |
| 1000, | |
| ]; | |
| const List<int> recordFieldCounts = [1, 2, 3, 4]; | |
| void generateBenchmarkClassesAndUtilities(IOSink output) { | |
| final maxCount = assertionCounts.reduce(max); | |
| output.writeln(''' | |
| // Copyright (c) 2023, 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. | |
| // | |
| // This benchmark suite measures the overhead of looking up elements of | |
| // SubtypeTestCaches, which are used when a type testing stub cannot determine | |
| // whether a given type is assignable. | |
| // | |
| // The Record lanes cover the case where there is no cache to look up: a record | |
| // has no class or type arguments to key an entry on, so those checks enter the | |
| // runtime every time. See dart-lang/sdk#61970. | |
| import 'package:benchmark_harness/benchmark_harness.dart'; | |
| void main() {'''); | |
| // We must run the benchmarks from smallest count to largest, since a single | |
| // STC is shared across all the benchmarks (due to the single call site in | |
| // [check]). This ensures that benchmarks that are testing counts small | |
| // enough for a linear STC use a linear STC. | |
| final sortedCounts = assertionCounts.toList(growable: false); | |
| sortedCounts.sort(); | |
| for (final count in sortedCounts) { | |
| output.write(''' | |
| const STC$count().report(); | |
| '''); | |
| } | |
| // We need to run the STCSame<max> benchmark only after running all the | |
| // STC<count> benchmarks, so that we ensure the shared STC is properly primed. | |
| output.write(''' | |
| const STCSame$maxCount().report(); | |
| '''); | |
| // Last, so the priming above is untouched. | |
| for (final fields in recordFieldCounts) { | |
| output.write(''' | |
| const Record$fields().report(); | |
| '''); | |
| } | |
| output.writeln(''' | |
| } | |
| class STCBenchmarkBase extends BenchmarkBase { | |
| final int count; | |
| const STCBenchmarkBase(String name, this.count) : super(name); | |
| // Normalize the cost across the benchmarks by number of type tests. | |
| @override | |
| void report() => emitter.emit(name, measure() / count); | |
| } | |
| // The record lanes loop rather than unroll: every check costs the same. | |
| const int checksPerRun = 1000; | |
| '''); | |
| for (final count in assertionCounts) { | |
| output.write(''' | |
| class STC$count extends STCBenchmarkBase { | |
| const STC$count() : super('$benchmarkName.STC$count', $count); | |
| @override | |
| void run() { | |
| '''); | |
| for (int i = 0; i < count; i++) { | |
| output.write(''' | |
| check<int>(instances[$i]); | |
| '''); | |
| } | |
| output.writeln(''' | |
| } | |
| } | |
| '''); | |
| } | |
| output.write(''' | |
| class STCSame$maxCount extends STCBenchmarkBase { | |
| const STCSame$maxCount() : super('$benchmarkName.STCSame$maxCount', $maxCount); | |
| @override | |
| void run() { | |
| // Do $maxCount AssertAssignable checks for the last type checked in the | |
| // STC$maxCount benchmark. | |
| '''); | |
| for (int i = 0; i < maxCount; i++) { | |
| output.write(''' | |
| check<int>(instances[${maxCount - 1}]); | |
| '''); | |
| } | |
| output.writeln(''' | |
| } | |
| } | |
| '''); | |
| for (final fields in recordFieldCounts) { | |
| output.write(''' | |
| class Record$fields extends STCBenchmarkBase { | |
| const Record$fields() : super('$benchmarkName.Record$fields', checksPerRun); | |
| @override | |
| void run() { | |
| for (int i = 0; i < checksPerRun; i++) { | |
| checkRecord$fields<int>(record$fields); | |
| } | |
| } | |
| } | |
| '''); | |
| } | |
| output.writeln(''' | |
| @pragma('vm:never-inline') | |
| @pragma('wasm:never-inline') | |
| @pragma('dart2js:never-inline') | |
| void check<S>(dynamic s) => s as C<S> Function(); | |
| class C<T> {} | |
| '''); | |
| for (int i = 0; i < maxCount; i++) { | |
| output.write(''' | |
| class C$i<T> extends C<T> {} | |
| C$i<S> closure$i<S>() => C$i<S>(); | |
| '''); | |
| } | |
| // We create constant tearoffs of the closures above to use for our values | |
| // in the `as` checks. We could make constant instances of the classes, but | |
| // the specialized TTS for the `C` class hierarchy means that we'll never | |
| // actually hit the SubtypeTestCache! | |
| // | |
| // Using closures both avoids the likelihood of eventually optimizing the TTS | |
| // for this check and making this benchmark outdated and also ensures the VM | |
| // performs the most intensive check for each STC entry, i.e., the | |
| // Subtype7TestCache stub is called. | |
| output.write(''' | |
| const instances = <dynamic>[ | |
| '''); | |
| for (int i = 0; i < maxCount; i++) { | |
| output.write(''' | |
| closure$i<int>, | |
| '''); | |
| } | |
| output.write(''' | |
| ]; | |
| '''); | |
| for (final fields in recordFieldCounts) { | |
| final type = fields == 1 | |
| ? '(S,)' | |
| : '(${List.filled(fields, 'S').join(', ')})'; | |
| final value = fields == 1 | |
| ? '(1,)' | |
| : '(${List.generate(fields, (i) => '${i + 1}').join(', ')})'; | |
| output.write(''' | |
| @pragma('vm:never-inline') | |
| @pragma('wasm:never-inline') | |
| @pragma('dart2js:never-inline') | |
| void checkRecord$fields<S>(dynamic s) => s as $type; | |
| final dynamic record$fields = $value; | |
| '''); | |
| } | |
| } | |
| void main() { | |
| final dartFilePath = path.join( | |
| path.dirname(Platform.script.path), | |
| 'dart', | |
| '$benchmarkName.dart', | |
| ); | |
| final dartSink = File(dartFilePath).openWrite(); | |
| generateBenchmarkClassesAndUtilities(dartSink); | |
| dartSink..flush(); | |
| } |