Skip to content

[benchmark_harness] Add blackhole#2410

Open
kevmoo wants to merge 3 commits into
mainfrom
black_hole
Open

[benchmark_harness] Add blackhole#2410
kevmoo wants to merge 3 commits into
mainfrom
black_hole

Conversation

@kevmoo
Copy link
Copy Markdown
Member

@kevmoo kevmoo commented May 20, 2026

Give benchmark writers a place to put results that help avoid compiler optimizations that can skew results

Give benchmark writers a place to put results that help avoid compiler optimizations that can skew results
@kevmoo kevmoo requested a review from a team as a code owner May 20, 2026 00:56
@github-actions
Copy link
Copy Markdown

Package publishing

Package Version Status Publish tag (post-merge)
package:bazel_worker 1.1.5 already published at pub.dev
package:benchmark_harness 2.5.0-wip WIP (no publish necessary)
package:boolean_selector 2.1.2 already published at pub.dev
package:browser_launcher 1.2.0-wip WIP (no publish necessary)
package:cli_config 0.2.1-wip WIP (no publish necessary)
package:cli_util 0.5.1 already published at pub.dev
package:clock 1.1.3-wip WIP (no publish necessary)
package:code_builder 4.12.0-wip WIP (no publish necessary)
package:coverage 1.15.0 already published at pub.dev
package:csslib 1.0.2 already published at pub.dev
package:extension_discovery 2.1.0 already published at pub.dev
package:file 7.0.2-wip WIP (no publish necessary)
package:file_testing 3.1.0-wip WIP (no publish necessary)
package:glob 2.1.3 already published at pub.dev
package:graphs 2.4.0-wip WIP (no publish necessary)
package:html 0.15.7-wip WIP (no publish necessary)
package:io 1.1.0-wip WIP (no publish necessary)
package:json_rpc_2 4.1.0 already published at pub.dev
package:markdown 7.4.0 ready to publish markdown-v7.4.0
package:mime 2.1.0-wip WIP (no publish necessary)
package:oauth2 2.0.5 already published at pub.dev
package:package_config 2.3.0-wip WIP (no publish necessary)
package:pool 1.5.3-wip WIP (no publish necessary)
package:process 5.0.5 (error) pubspec version (5.0.5) and changelog (5.0.6-wip) don't agree
package:pub_semver 2.2.0 already published at pub.dev
package:pubspec_parse 1.6.0-wip WIP (no publish necessary)
package:source_map_stack_trace 2.1.3-wip WIP (no publish necessary)
package:source_maps 0.10.14-wip WIP (no publish necessary)
package:source_span 1.10.2 already published at pub.dev
package:sse 4.2.0 already published at pub.dev
package:stack_trace 1.12.2-wip (error) pubspec version (1.12.2-wip) and changelog (1.12.2-dev) don't agree
package:stream_channel 2.1.4 already published at pub.dev
package:stream_transform 2.1.2-wip WIP (no publish necessary)
package:string_scanner 1.4.2-wip WIP (no publish necessary)
package:term_glyph 1.2.3-wip WIP (no publish necessary)
package:test_reflective_loader 0.6.0 ready to publish test_reflective_loader-v0.6.0
package:timing 1.0.2 already published at pub.dev
package:unified_analytics 8.0.15 ready to publish unified_analytics-v8.0.15
package:watcher 1.2.2-wip WIP (no publish necessary)
package:yaml 3.1.4-wip WIP (no publish necessary)
package:yaml_edit 2.2.4 already published at pub.dev

Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 20, 2026

PR Health

License Headers ✔️
// Copyright (c) 2026, 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.

Files
no missing headers

All source files should start with a license header.

Unrelated files missing license headers
Files
pkgs/bazel_worker/benchmark/benchmark.dart
pkgs/coverage/lib/src/coverage_options.dart
pkgs/html/example/main.dart
pkgs/pubspec_parse/test/git_uri_test.dart
pkgs/watcher/test/custom_watcher_factory_test.dart

This check can be disabled by tagging the PR with skip-license-check.

Breaking changes ✔️
Package Change Current Version New Version Needed Version Looking good?
benchmark_harness Non-Breaking 2.4.0 2.5.0-wip 2.5.0-wip ✔️

This check can be disabled by tagging the PR with skip-breaking-check.

Coverage ⚠️
File Coverage
pkgs/benchmark_harness/lib/benchmark_harness.dart 💔 Not covered
pkgs/benchmark_harness/lib/src/async_benchmark_base.dart 💚 88 % ⬆️ 1 %
pkgs/benchmark_harness/lib/src/benchmark_base.dart 💔 81 % ⬇️ 4 %
pkgs/benchmark_harness/lib/src/blackhole.dart 💚 75 %
pkgs/benchmark_harness/lib/src/perf_benchmark_base.dart 💔 Not covered

This check for test coverage is informational (issues shown here will not fail the PR).

This check can be disabled by tagging the PR with skip-coverage-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

Unused Dependencies ⚠️
Package Status
benchmark_harness
❗ Show Issues
These packages may be unused, or you may be using assets from these packages:
* path

For details on how to fix these, see dependency_validator.

This check can be disabled by tagging the PR with skip-unused-dependencies-check.

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

@kevmoo
Copy link
Copy Markdown
Member Author

kevmoo commented May 20, 2026

@mraleph @lrhn – figured ya'll would have thoughts here, too!

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a blackhole utility to the benchmark_harness package to prevent dead-code elimination (DCE) in microbenchmarks. It includes the implementation of the Blackhole class, integration into sync and async benchmark bases, and comprehensive tests. Review feedback focuses on ensuring the effectiveness of the DCE prevention by moving preventDCE() calls to after the measurement loops and refining the Blackhole class implementation for better type safety and reduced redundancy.

Comment thread pkgs/benchmark_harness/lib/src/benchmark_base.dart Outdated
Comment thread pkgs/benchmark_harness/lib/src/benchmark_base.dart Outdated
Comment thread pkgs/benchmark_harness/lib/src/async_benchmark_base.dart Outdated
Comment thread pkgs/benchmark_harness/lib/src/async_benchmark_base.dart Outdated
Comment thread pkgs/benchmark_harness/lib/src/blackhole.dart Outdated
@kevmoo
Copy link
Copy Markdown
Member Author

kevmoo commented May 20, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a blackhole utility to prevent dead-code elimination in microbenchmarks, integrating it into the measurement loops of BenchmarkBase, AsyncBenchmarkBase, and PerfBenchmarkBase. The reviewer recommends refactoring the Blackhole class into top-level functions to align with Effective Dart guidelines and suggests using a more robust opaque condition to ensure the utility remains effective against modern compiler optimizations.

Comment on lines +33 to +58
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) {
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) {
Blackhole._sink = value;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 int.tryParse('0') == 1 might be constant-folded by modern compilers; using something like DateTime.now().year < 0 would be more robust for preventing dead-code elimination across different platforms.

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
  1. AVOID defining a class that contains only static members. (link)
  2. Prioritize robust, future-proof implementations for foundational components to avoid issues like dead-code elimination.

}
return elapsed / iter;
} finally {
Blackhole.preventDCE();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Call the top-level preventDCE() function directly instead of using the static class member.

Suggested change
Blackhole.preventDCE();
preventDCE();

return await measureFor(exercise, 2000);
} finally {
await teardown();
Blackhole.preventDCE();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Call the top-level preventDCE() function directly instead of using the static class member.

Suggested change
Blackhole.preventDCE();
preventDCE();

try {
return measureForImpl(f, minimumMillis).score;
} finally {
Blackhole.preventDCE();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Call the top-level preventDCE() function directly instead of using the static class member.

Suggested change
Blackhole.preventDCE();
preventDCE();

return result.score;
} finally {
teardown();
Blackhole.preventDCE();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Call the top-level preventDCE() function directly instead of using the static class member.

Suggested change
Blackhole.preventDCE();
preventDCE();

}
} finally {
teardown();
Blackhole.preventDCE();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Call the top-level preventDCE() function directly instead of using the static class member.

Suggested change
Blackhole.preventDCE();
preventDCE();

});
});

group('Blackhole class preventDCE', () {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the group name to reflect the move to a top-level function.

Suggested change
group('Blackhole class preventDCE', () {
group('preventDCE', () {


group('Blackhole class preventDCE', () {
test('preventDCE executes successfully without throwing errors', () {
expect(Blackhole.preventDCE, returnsNormally);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Test the top-level preventDCE() function directly.

Suggested change
expect(Blackhole.preventDCE, returnsNormally);
expect(preventDCE, returnsNormally);

static void preventDCE() {
// Opaque condition that is always false at runtime but unresolvable
// at compile-time.
if (int.tryParse('0') == 1) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't assume compilers can't optimize int.parse.

Consider if (DateTime.now().millisecondsSinceEpoch < 0).
That should not ever be compile-time optimized.

}
}

/// A zero-cost compiler-safe live sink to prevent dead-code elimination.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why/how does it prevent dead-code elemination?

iter++;
}
return elapsed / iter;
} finally {
Copy link
Copy Markdown
Member

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/finally can affect compilation. I wouldn't do that in a benchmark.

If anything, put it in a catch clause and before the return. finally is odd to optimize.

}
return elapsed / iter;
} finally {
Blackhole.preventDCE();
Copy link
Copy Markdown
Member

@lrhn lrhn May 20, 2026

Choose a reason for hiding this comment

The 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 f(), and f doesn't have visible side effects (which none of the benchmarked functions in a function likely has), then the compiler can just not call f() more than once (just to see if it throws).

Don't assume compilers are stupid. That can always change in the future.

Copy link
Copy Markdown
Member

@lrhn lrhn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why this ensures anything.
Calling Blackhole.preventDCS will probably prevent that being optimized away, but there is no data flow from the benchmark into the call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants