Implement the branch-hinting proposal (parse + Cranelift cold blocks)#13459
Implement the branch-hinting proposal (parse + Cranelift cold blocks)#13459gfx wants to merge 2 commits into
Conversation
Parse the `metadata.code.branch_hint` custom section and use it to mark cold blocks during Cranelift compilation. Hints are applied to `br_if` and `if` (including the lazily-allocated `else` case) and consumed in program-counter order via a forward cursor (O(n), no per-offset map). Gated behind a new `Config::wasm_branch_hinting` knob (stored in `Tunables`, default off until the proposal is fuzzed) with matching CLI `-Wbranch-hinting` and C API. Hints are advisory and never affect execution semantics, so the setting is excluded from artifact compatibility checks. Tests: disas snapshots for x86_64 and aarch64, plus an API test asserting semantic neutrality across enabled/disabled/default configs. Follow-ups: malformed-section validation + spec-test ungating, wasm-smith emission + fuzzing, then enabling by default. Refs bytecodealliance#9463 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4cb0651 to
cd4b91c
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds opt-in support for the WebAssembly branch-hinting proposal by parsing metadata.code.branch_hint and using it to mark cold blocks during Cranelift compilation, with corresponding user-facing knobs and regression tests.
Changes:
- Introduces a
Config::wasm_branch_hintingtoggle (plus CLI/C-API plumbing) to enable/disable parsing and use of branch-hint metadata. - Parses the
metadata.code.branch_hintcustom section into per-function hint lists and threads them through Cranelift translation to mark cold blocks forif/br_if. - Adds disassembly and runtime tests (including a “disabled by default” case) plus release/docs updates.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/disas/branch-hinting.wat | New disas test ensuring hints mark the expected cold blocks when enabled. |
| tests/disas/branch-hinting-disabled.wat | New disas regression test ensuring hints are ignored by default (no cold blocks). |
| tests/disas/branch-hinting-aarch64.wat | New backend coverage showing hint-driven cold-path layout on aarch64. |
| tests/all/main.rs | Wires in the new Rust test module. |
| tests/all/branch_hinting.rs | Adds semantic-neutrality test across enabled/disabled/default configurations. |
| docs/stability-wasm-proposals.md | Documents branch-hinting status and adds tracking footnote. |
| crates/wasmtime/src/engine/serialization.rs | Treats branch_hinting as non-compat-affecting for serialized artifacts. |
| crates/wasmtime/src/config.rs | Adds Config::wasm_branch_hinting public API. |
| crates/environ/src/tunables.rs | Adds branch_hinting tunable with default false. |
| crates/environ/src/compile/module_environ.rs | Stores parsed branch hints in ModuleTranslation when enabled. |
| crates/cranelift/src/translate/stack.rs | Tracks deferred “else is cold” state for lazily-allocated else blocks. |
| crates/cranelift/src/translate/code_translator.rs | Applies branch hints for if and br_if by marking cold successor blocks. |
| crates/cranelift/src/func_environ.rs | Adds per-function hint cursoring and take_branch_hint lookup by srcloc. |
| crates/cranelift/src/compiler.rs | Feeds per-function hints and function-body start offset into FuncEnvironment. |
| crates/cli-flags/src/lib.rs | Exposes -Wbranch-hinting / wasm proposal flag wiring (opt-in, not in “all”). |
| crates/c-api/src/config.rs | Adds C API setter to enable/disable branch-hinting. |
| crates/c-api/include/wasmtime/config.h | Declares the new C API configuration property. |
| RELEASES.md | Notes the new opt-in branch-hinting support in release notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Branch hints parsed from the `metadata.code.branch_hint` custom section, | ||
| /// keyed by module-level function index. Only populated when | ||
| /// [`Tunables::branch_hinting`] is enabled. | ||
| pub branch_hints: HashMap<FuncIndex, Box<[BranchHint]>>, |
Review feedback on bytecodealliance#13459: a malformed `metadata.code.branch_hint` section could list the same function twice, and `HashMap::insert` would silently drop the earlier entry. Use `entry().or_insert` so duplicates are handled deterministically (first occurrence wins). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Label Messager: wasmtime:configIt looks like you are changing Wasmtime's configuration options. Make sure to
DetailsTo modify this label's message, edit the To add new label messages or remove existing label messages, edit the |
Refs #9463. Supersedes #12483 (closed); this reworks that attempt to address
the review there.
What this does
Parses the
metadata.code.branch_hintcustom section and uses the hints to markcold blocks during Cranelift compilation, behind a new opt-in knob:
Config::wasm_branch_hinting(bool)— off by default. Also-W branch-hinting(CLI) and
wasmtime_config_wasm_branch_hinting_set(C API).br_ifandif(the proposal hints both). For a hintedbranch, the unlikely successor is marked cold via
FunctionBuilder::set_cold_block.today, and when on it never changes execution semantics.
Addressing the #12483 review
ascending PC order, so they are consumed with a forward cursor as translation
visits increasing offsets (
FuncEnvironment::take_branch_hint) — O(1)amortized, O(n) per function, no
HashMap<offset, _>. We rely on the spec'sordering rather than re-sorting (validating malformed sections is left to a
follow-up, see below).
WasmFeaturesbit forbranch hinting (it is a pure custom section with nothing to validate in
wasmparser), so the knob lives inTunablesand is read by both the parser(
ModuleEnvironment) and the compiler (FuncEnvironment). Per the proposalstability rules it stays off until fuzzed.
ifis handled (a gap in Implement branch hinting proposal support #12483, which only didbr_if): all hints intests/spec_testsuite/custom/branch_hint.wastare onif. Theifcaseincludes the subtlety that the
elseblock is sometimes allocated lazily(when
params == results); a likely-taken hint there is deferred viaControlStackFrame::If { else_is_cold }and applied whenOperator::Elsecreates the block.
The hint offset uses
builder.srcloc().bits()(module-relative) minus thefunction body's start (
body.get_binary_reader().original_position()), matchingwasmparser::BranchHint.func_offset.Config::wasm_branch_hintingonly affects cold-block layout, so it is excludedfrom the compiled-artifact compatibility check in
engine/serialization.rs(an artifact loads into an engine configured either way).
Codegen evidence
In-tree,
tests/disas/branch-hinting-aarch64.watchecks$if_unlikelywith theknob on: the unlikely then-block is hoisted out of line past
ret, so the hotpath falls through.
tests/disas/branch-hinting-disabled.watis the same modulewithout the flag and asserts that nothing is marked cold.
The off-vs-on contrast is clearest on a hot loop — from the objdump A/B in the
dogfood experiment linked below, per iteration the hot path goes from two taken
branches to one:
Performance
The transformation is the expected one (hot path tightened, cold path moved out
of line), but I'm not claiming a microbenchmark number here: on a tight loop on
Apple Silicon the wall-time delta is within noise — the eliminated forward
branch is perfectly predicted and the loop fits in L1i, so layout barely
matters. Branch hints pay off under i-cache pressure / large hot code, so the
credible place for a number is
/bench_x64on a representative workload ratherthan a microbenchmark. There is no runtime overhead either way: hints only
affect compile-time block layout, and with the knob off codegen is unchanged.
This was dogfooded end-to-end against Wado
output (a language that emits
metadata.code.branch_hint): an objdump A/B on areal Wado component shows the hints take effect. The harness and numbers are
published as a reproducible experiment:
wado-lang/wado#1152.
Testing
tests/disas/branch-hinting.wat(x86_64, 5 cases):br_if/if× likely/unlikely,plus the lazily-allocated-
elsecase — each marks the expected block cold.tests/disas/branch-hinting-aarch64.wat(aarch64,test = "compile"): coldpaths laid out-of-line in real machine code (the optimization is in
target-independent Cranelift, so it applies on every backend).
tests/disas/branch-hinting-disabled.wat: the same hinted module with the knoboff produces no cold blocks (no-regression).
tests/all/branch_hinting.rs: a hinted module produces identical results withthe knob enabled, explicitly disabled, and at its default — confirming
semantic neutrality.
Scope and follow-ups
This is intentionally the codegen + knob slice. Tracked as follow-ups before
flipping the default on:
br_ifand
ifonly).branch_hint.wastexercises bothassert_malformed_custom(duplicate hint, hint outside a function) and
assert_invalid_custom(hintwhose offset points at a non-branch instruction). Phase 1 ignores misplaced
hints rather than rejecting them; making these pass needs the validation plus
assert_{malformed,invalid}_customsupport in the wast runner, then ungatingthe test (currently skipped in
crates/test-util/src/wast.rs).wasm-smithbranch-hint emission + fuzzing soak, then enable by default andupdate
module_generation_uses_expected_proposals.The stability matrix is updated accordingly (Tier 3, API/C-API done, Tests/Fuzzed
pending, off by default).