-
Notifications
You must be signed in to change notification settings - Fork 91
[benchmark_harness] Add blackhole
#2410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| // 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 'blackhole.dart'; | ||
| import 'score_emitter.dart'; | ||
|
|
||
| class AsyncBenchmarkBase { | ||
|
|
@@ -39,16 +40,20 @@ class AsyncBenchmarkBase { | |
| Future<void> Function() f, | ||
| int minimumMillis, | ||
| ) async { | ||
| final minimumMicros = minimumMillis * 1000; | ||
| final watch = Stopwatch()..start(); | ||
| var iter = 0; | ||
| var elapsed = 0; | ||
| while (elapsed < minimumMicros) { | ||
| await f(); | ||
| elapsed = watch.elapsedMicroseconds; | ||
| iter++; | ||
| try { | ||
| final minimumMicros = minimumMillis * 1000; | ||
| final watch = Stopwatch()..start(); | ||
| var iter = 0; | ||
| var elapsed = 0; | ||
| while (elapsed < minimumMicros) { | ||
| await f(); | ||
| elapsed = watch.elapsedMicroseconds; | ||
| iter++; | ||
| } | ||
| return elapsed / iter; | ||
| } finally { | ||
| Blackhole.preventDCE(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not depend on anything inside the loop, so a compiler can still eliminate the loop content. If the post-benchmark code does not depend on the output of Don't assume compilers are stupid. That can always change in the future. |
||
| } | ||
| return elapsed / iter; | ||
| } | ||
|
|
||
| /// Measures the score for the benchmark and returns it. | ||
|
|
@@ -61,6 +66,7 @@ class AsyncBenchmarkBase { | |
| return await measureFor(exercise, 2000); | ||
| } finally { | ||
| await teardown(); | ||
| Blackhole.preventDCE(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| // 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 'blackhole.dart'; | ||
| import 'measurement.dart'; | ||
| import 'score_emitter.dart'; | ||
|
|
||
|
|
@@ -42,18 +43,27 @@ class BenchmarkBase { | |
|
|
||
| /// Measures the score for this benchmark by executing it repeatedly until | ||
| /// time minimum has been reached. | ||
| static double measureFor(void Function() f, int minimumMillis) => | ||
| measureForImpl(f, minimumMillis).score; | ||
| static double measureFor(void Function() f, int minimumMillis) { | ||
| try { | ||
| return measureForImpl(f, minimumMillis).score; | ||
| } finally { | ||
| Blackhole.preventDCE(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
|
|
||
| /// Measures the score for the benchmark and returns it. | ||
| double measure() { | ||
| setup(); | ||
| // Warmup for at least 100ms. Discard result. | ||
| measureForImpl(warmup, 100); | ||
| // Run the benchmark for at least 2000ms. | ||
| var result = measureForImpl(exercise, minimumMeasureDurationMillis); | ||
| teardown(); | ||
| return result.score; | ||
| try { | ||
| // Warmup for at least 100ms. Discard result. | ||
| measureForImpl(warmup, 100); | ||
| // Run the benchmark for at least 2000ms. | ||
| var result = measureForImpl(exercise, minimumMeasureDurationMillis); | ||
| return result.score; | ||
| } finally { | ||
| teardown(); | ||
| Blackhole.preventDCE(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
|
|
||
| void report() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // 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. | ||
|
|
||
| // NOTE: Upgrading to Dart 3.12+ | ||
| // | ||
| // When the package's minimum supported SDK version is bumped to Dart 3.12 | ||
| // or higher (which fully supports `@pragma('external-effect')` natively), this | ||
| // class can be simplified to a pure zero-cost external function invocation: | ||
| // | ||
| // ```dart | ||
| // @pragma('vm:prefer-inline') | ||
| // @pragma('dart2js:prefer-inline') | ||
| // @pragma('wasm:prefer-inline') | ||
| // void blackhole(Object? value) { | ||
| // _BlackholeSink._reach(value); | ||
| // } | ||
| // | ||
| // class _BlackholeSink { | ||
| // @pragma('external-effect') | ||
| // external static void _reach(Object? value); | ||
| // } | ||
| // ``` | ||
| // | ||
| // Until then, we use a highly optimized static dynamic sink with an opaque | ||
| // read guard that is fully compatible with Dart 3.10 and 3.11. | ||
|
|
||
| /// A compiler-recognized zero-cost live sink to prevent dead-code elimination. | ||
| /// | ||
| /// Passing a value to [blackhole] ensures that the compiler treats the value's | ||
| /// computation as live, preventing tree-shaking and dead-code elimination, | ||
| /// while introducing zero runtime execution overhead. | ||
| class Blackhole { | ||
| static Object? _sink; | ||
|
|
||
| /// An opaque guard that convinces compiler static analyses (such as TFA) | ||
| /// that [_sink] is read, preventing it from being tree-shaken as write-only. | ||
| /// | ||
| /// Automatically invoked inside benchmark measurement loops. | ||
| @pragma('vm:never-inline') | ||
| @pragma('dart2js:never-inline') | ||
| @pragma('wasm:never-inline') | ||
| static void preventDCE() { | ||
| // Opaque condition that is always false at runtime but unresolvable | ||
| // at compile-time. | ||
| if (int.tryParse('0') == 1) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't assume compilers can't optimize Consider |
||
| print(_sink); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// A zero-cost compiler-safe live sink to prevent dead-code elimination. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why/how does it prevent dead-code elemination? |
||
| @pragma('vm:prefer-inline') | ||
| @pragma('dart2js:prefer-inline') | ||
| @pragma('wasm:prefer-inline') | ||
| void blackhole(Object? value) { | ||
| Blackhole._sink = value; | ||
| } | ||
|
Comment on lines
+33
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Dart, it is more idiomatic to use top-level functions and variables instead of a class that contains only static members. This follows the Effective Dart recommendation. Additionally, the opaque condition Object? _sink;
/// An opaque guard that convinces compiler static analyses (such as TFA)
/// that [_sink] is read, preventing it from being tree-shaken as write-only.
///
/// Automatically invoked inside benchmark measurement loops.
@pragma('vm:never-inline')
@pragma('dart2js:never-inline')
@pragma('wasm:never-inline')
void preventDCE() {
// Opaque condition that is always false at runtime but unresolvable
// at compile-time.
if (DateTime.now().year < 0) {
print(_sink);
}
}
/// A zero-cost compiler-safe live sink to prevent dead-code elimination.
@pragma('vm:prefer-inline')
@pragma('dart2js:prefer-inline')
@pragma('wasm:prefer-inline')
void blackhole(Object? value) {
_sink = value;
}References
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import 'dart:convert'; | |
| import 'dart:io'; | ||
|
|
||
| import 'benchmark_base.dart'; | ||
| import 'blackhole.dart'; | ||
| import 'measurement.dart'; | ||
| import 'score_emitter.dart'; | ||
|
|
||
|
|
@@ -129,6 +130,7 @@ class PerfBenchmarkBase extends BenchmarkBase { | |
| } | ||
| } finally { | ||
| teardown(); | ||
| Blackhole.preventDCE(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| return result.score; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // 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. | ||
|
|
||
| import 'package:benchmark_harness/benchmark_harness.dart'; | ||
| import 'package:benchmark_harness/src/blackhole.dart'; | ||
| import 'package:test/test.dart'; | ||
|
|
||
| class SampleBenchmark extends BenchmarkBase { | ||
| int result = 0; | ||
| SampleBenchmark() : super('Sample'); | ||
|
|
||
| @override | ||
| void run() { | ||
| result = 42 + 58; | ||
| blackhole(result); | ||
| } | ||
| } | ||
|
|
||
| class SampleAsyncBenchmark extends AsyncBenchmarkBase { | ||
| int result = 0; | ||
| SampleAsyncBenchmark() : super('SampleAsync'); | ||
|
|
||
| @override | ||
| Future<void> run() async { | ||
| await Future<void>.delayed(const Duration(microseconds: 1)); | ||
| result = 100; | ||
| blackhole(result); | ||
| } | ||
| } | ||
|
|
||
| void main() { | ||
| group('blackhole shorthand', () { | ||
| test('executes successfully without throwing errors', () { | ||
| expect(() => blackhole('test-string'), returnsNormally); | ||
| expect(() => blackhole(null), returnsNormally); | ||
| expect(() => blackhole(42), returnsNormally); | ||
| }); | ||
| }); | ||
|
|
||
| group('Blackhole class preventDCE', () { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| test('preventDCE executes successfully without throwing errors', () { | ||
| expect(Blackhole.preventDCE, returnsNormally); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }); | ||
| }); | ||
|
|
||
| group('Benchmark Integration', () { | ||
| test('sync benchmark using blackhole runs and measures successfully', () { | ||
| final benchmark = SampleBenchmark(); | ||
| final score = benchmark.measure(); | ||
| expect(score, isPositive); | ||
| expect(benchmark.result, equals(100)); | ||
| }); | ||
|
|
||
| test( | ||
| 'async benchmark using blackhole runs and measures successfully', | ||
| () async { | ||
| final benchmark = SampleAsyncBenchmark(); | ||
| final score = await benchmark.measure(); | ||
| expect(score, isPositive); | ||
| expect(benchmark.result, equals(100)); | ||
| }, | ||
| ); | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Putting code inside a
try/finallycan affect compilation. I wouldn't do that in a benchmark.If anything, put it in a
catchclause and before the return.finallyis odd to optimize.