gfx opened PR #13459 from wado-lang:gfx/branch-hinting-v2 to bytecodealliance:main:
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 mark
cold blocks during Cranelift compilation, behind a new opt-in knob:
Config::wasm_branch_hinting(bool)— off by default. Also-W branch-hinting
(CLI) andwasmtime_config_wasm_branch_hinting_set(C API).Applies to both
br_ifandif(the proposal hints both). For a hinted
branch, the unlikely successor is marked cold viaFunctionBuilder::set_cold_block.Hints are advisory: when the knob is off the section is ignored exactly as
today, and when on it never changes execution semantics.Addressing the #12483 review
O(n), no per-offset map (cfallin/alexcrichton): the proposal lists hints in
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, noHashMap<offset, _>. We rely on the spec's
ordering rather than re-sorting (validating malformed sections is left to a
follow-up, see below).Config knob, default off (fitzgen): there is no
WasmFeaturesbit for
branch 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 proposal
stability rules it stays off until fuzzed.
ifis handled (a gap in #12483, which only didbr_if): all hints in
tests/spec_testsuite/custom/branch_hint.wastare onif. Theifcase
includes the subtlety that theelseblock is sometimes allocated lazily
(whenparams == results); a likely-taken hint there is deferred via
ControlStackFrame::If { else_is_cold }and applied whenOperator::Else
creates the block.The hint offset uses
builder.srcloc().bits()(module-relative) minus the
function body's start (body.get_binary_reader().original_position()), matching
wasmparser::BranchHint.func_offset.
Config::wasm_branch_hintingonly affects cold-block layout, so it is excluded
from the compiled-artifact compatibility check inengine/serialization.rs
(an artifact loads into an engine configured either way).Codegen evidence
In-tree,
tests/disas/branch-hinting-aarch64.watchecks$if_unlikelywith the
knob on: the unlikely then-block is hoisted out of line pastret, so the hot
path falls through.tests/disas/branch-hinting-disabled.watis the same module
without 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:off — cold inline, hot path branches over it every iteration: cbnz x0, <cold> ; ~always taken, skip the inline cold body <cold body> ; wedged between the hot body and the loop back-edge add x3, #1 b <loop> on — cold hoisted past `ret`, hot path falls through: cbz x0, <cold> ; ~never taken add x3, #1 b <loop> ret <cold body> ; out of linePerformance
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 rather
than 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 emitsmetadata.code.branch_hint): an objdump A/B on a
real Wado component shows the hints take effect. The harness and numbers are
published as a reproducible experiment:
<https://github.com/wado-lang/wado/pull/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"): cold
paths 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 knob
off produces no cold blocks (no-regression).
tests/all/branch_hinting.rs: a hinted module produces identical results with
the 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:
Validate the section and reject bad hints (the spec restricts hints to
br_if
andifonly).branch_hint.wastexercises bothassert_malformed_custom
(duplicate hint, hint outside a function) andassert_invalid_custom(hint
whose 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 ungating
the test (currently skipped incrates/test-util/src/wast.rs).
wasm-smithbranch-hint emission + fuzzing soak, then enable by default and
updatemodule_generation_uses_expected_proposals.The stability matrix is updated accordingly (Tier 3, API/C-API done, Tests/Fuzzed
pending, off by default).
gfx requested Copilot for a review on PR #13459.
gfx requested fitzgen for a review on PR #13459.
gfx requested wasmtime-core-reviewers for a review on PR #13459.
gfx requested wasmtime-compiler-reviewers for a review on PR #13459.
gfx requested wasmtime-default-reviewers for a review on PR #13459.
gfx updated PR #13459.
:memo: Copilot submitted PR review:
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_hintand 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.
<details>
<summary>Show a summary per file</summary>
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 coldblocks).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_hintingas non-compat-affecting for serialized artifacts.crates/wasmtime/src/config.rs Adds Config::wasm_branch_hintingpublic API.crates/environ/src/tunables.rs Adds branch_hintingtunable with defaultfalse.crates/environ/src/compile/module_environ.rs Stores parsed branch hints in ModuleTranslationwhen 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 ifandbr_ifby marking cold successor blocks.crates/cranelift/src/func_environ.rs Adds per-function hint cursoring and take_branch_hintlookup 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. </details>
:light_bulb: <a href="/bytecodealliance/wasmtime/new/main?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
:speech_balloon: Copilot created PR review comment:
Making
ModuleTranslation::branch_hintsa public field exposes an internal representation (and its invariants, e.g. sorted offsets) as API surface. Consider keeping the field private and providing a small accessor (e.g., lookup byFuncIndexreturning&[BranchHint]) so you can change storage/validation strategy later without forcing downstream changes.
:speech_balloon: Copilot created PR review comment:
Using
HashMap::insertwill overwrite any previously-parsed hints for the same function index, silently dropping earlier entries if the custom section contains duplicates. Even if full validation is deferred, it would be safer to either (a) append/extend an existing hint list (entry(...).and_modify(|v| ...).or_insert(...)) or (b) ignore/log duplicates deterministically to avoid losing hints unexpectedly.
gfx updated PR #13459.
github-actions[bot] added the label wasmtime:api on PR #13459.
github-actions[bot] added the label wasmtime:c-api on PR #13459.
github-actions[bot] added the label wasmtime:config on PR #13459.
github-actions[bot] added the label wasmtime:docs on PR #13459.
github-actions[bot] commented on PR #13459:
Label Messager: wasmtime:config
It looks like you are changing Wasmtime's configuration options. Make sure to
complete this check list:
[ ] If you added a new
Configmethod, you wrote extensive documentation for
it.<details>
Our documentation should be of the following form:
```text
Short, simple summary sentence.More details. These details can be multiple paragraphs. There should be
information about not just the method, but its parameters and results as
well.Is this method fallible? If so, when can it return an error?
Can this method panic? If so, when does it panic?
Example
Optional example here.
```</details>
[ ] If you added a new
Configmethod, or modified an existing one, you
ensured that this configuration is exercised by the fuzz targets.<details>
For example, if you expose a new strategy for allocating the next instance
slot inside the pooling allocator, you should ensure that at least one of our
fuzz targets exercises that new strategy.Often, all that is required of you is to ensure that there is a knob for this
configuration option in [wasmtime_fuzzing::Config][fuzzing-config] (or one
of its nestedstructs).Rarely, this may require authoring a new fuzz target to specifically test this
configuration. See [our docs on fuzzing][fuzzing-docs] for more details.</details>
[ ] If you are enabling a configuration option by default, make sure that it
has been fuzzed for at least two weeks before turning it on by default.[fuzzing-config]: https://github.com/bytecodealliance/wasmtime/blob/ca0e8d0a1d8cefc0496dba2f77a670571d8fdcab/crates/fuzzing/src/generators.rs#L182-L194
[fuzzing-docs]: https://docs.wasmtime.dev/contributing-fuzzing.html
<details>
To modify this label's message, edit the <code>.github/label-messager/wasmtime-config.md</code> file.
To add new label messages or remove existing label messages, edit the
<code>.github/label-messager.json</code> configuration file.</details>
alexcrichton commented on PR #13459:
@gfx please take a moment to acquaint yourself with the BA's AI tool use policy. Notably this looks like a very large wall of text generated by an AI. Please ensure that you are yourself in the loop on all communication because you, after all, own this change and are responsible for it.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
This is quite wordy -- this just needs to refer to the tracking issue.
:speech_balloon: alexcrichton created PR review comment:
You'll want to update at least the first two, possibly third, entry in this matrix. The proposal is phase 5 and this PR includes tests. For "Finished" either the footnote should explicitly say what remains, or it should be checked off.
:speech_balloon: alexcrichton created PR review comment:
Can this pass
-Wbranch-hinting=nin theflagssection? Also please make sure to phrase comments for readers of the code, not reviewers of the PR. This is committed documentation and does not need to refer to historical state which no one will remember once this lands. This only needs to indicate, for example, that this is testing codegen specifically ignores the custom section when the feature is disabled.
:speech_balloon: alexcrichton created PR review comment:
Can this test be extracted to a
*.wastfile?
:speech_balloon: alexcrichton created PR review comment:
Instead of returning
Option<bool>, which an be confusing, could this yield aBranchHintitself directly? That way thetakenfield can be used and english-wise understoood quickly.
:speech_balloon: alexcrichton created PR review comment:
Additionally, to avoid extra overhead, I'd recommend storing the raw decoder itself and saving that in a side table. There shouldn't be a need here to decode the entire section and heap-allocate it all in Wasmtime.
:speech_balloon: alexcrichton created PR review comment:
This comment can be misleading because Wasmtime, spec-wise, can't guarantee anything or require anything of custom sections. This implementation does not validate branch hints nor does it fail on any malformed input. It should be sufficient to say that this ignores invalid sections and more generally does not perform any validation at all.
:speech_balloon: alexcrichton created PR review comment:
Rather than making a dedicated method for this, could the construction and initialization happen in the constructor for this type?
:speech_balloon: alexcrichton created PR review comment:
Can this loop explicitly handle errors rather than discarding them with
.flatten()? For example if the branch-hinting section is corrupt then the entire section should be discarded.
gfx converted PR #13459 Implement the branch-hinting proposal (parse + Cranelift cold blocks) to a draft
gfx updated PR #13459.
github-actions[bot] added the label fuzzing on PR #13459.
github-actions[bot] commented on PR #13459:
Subscribe to Label Action
cc @fitzgen
<details>
This issue or pull request has been labeled: "fuzzing", "wasmtime:api", "wasmtime:c-api", "wasmtime:config", "wasmtime:docs"Thus the following users have been cc'd because of the following labels:
- fitzgen: fuzzing
To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.
Learn more.
</details>
:thumbs_up: alexcrichton submitted PR review:
Thanks! This all looks good to me. I've got one possible touch-up with
peekablebelow, but otherwise with acargo fmtthis seems reasonable to land
:speech_balloon: alexcrichton created PR review comment:
One option here would be to use the
Iterator::peekablemethod in the standard library to avoid thepeeked_hintfield and make it a bit easier to usebranch_hintsperhaps?
gfx updated PR #13459.
gfx updated PR #13459.
gfx has marked PR #13459 as ready for review.
gfx requested wasmtime-fuzz-reviewers for a review on PR #13459.
Rebased onto main (conflicts from #13487 and #13354 resolved), all review comments addressed, and cold-block layout is now covered across the x86-64, aarch64, and Pulley backends. CI is green. I think this is ready to merge!
gfx edited PR #13459:
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 mark
cold blocks during Cranelift compilation, behind a new opt-in knob:
Config::wasm_branch_hinting(bool)— off by default. Also-W branch-hinting
(CLI) andwasmtime_config_wasm_branch_hinting_set(C API).Applies to both
br_ifandif(the proposal hints both). For a hinted
branch, the unlikely successor is marked cold viaFunctionBuilder::set_cold_block.Hints are advisory: when the knob is off the section is ignored exactly as
today, and when on it never changes execution semantics.Addressing the #12483 review
O(n), no per-offset map (cfallin/alexcrichton): the proposal lists hints in
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, noHashMap<offset, _>. We rely on the spec's
ordering rather than re-sorting (validating malformed sections is left to a
follow-up, see below).Config knob, default off (fitzgen): there is no
WasmFeaturesbit for
branch 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 proposal
stability rules it stays off until fuzzed.
ifis handled (a gap in #12483, which only didbr_if): all hints in
tests/spec_testsuite/custom/branch_hint.wastare onif. Theifcase
includes the subtlety that theelseblock is sometimes allocated lazily
(whenparams == results); a likely-taken hint there is deferred via
ControlStackFrame::If { else_is_cold }and applied whenOperator::Else
creates the block.The hint offset uses
builder.srcloc().bits()(module-relative) minus the
function body's start (body.get_binary_reader().original_position()), matching
wasmparser::BranchHint.func_offset.
Config::wasm_branch_hintingonly affects cold-block layout, so it is excluded
from the compiled-artifact compatibility check inengine/serialization.rs
(an artifact loads into an engine configured either way).Codegen evidence
In-tree,
tests/disas/branch-hinting-aarch64.watchecks$if_unlikelywith the
knob on: the unlikely then-block is hoisted out of line pastret, so the hot
path falls through.tests/disas/branch-hinting-disabled.watis the same module
without 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:off — cold inline, hot path branches over it every iteration: cbnz x0, <cold> ; ~always taken, skip the inline cold body <cold body> ; wedged between the hot body and the loop back-edge add x3, #1 b <loop> on — cold hoisted past `ret`, hot path falls through: cbz x0, <cold> ; ~never taken add x3, #1 b <loop> ret <cold body> ; out of linePerformance
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 rather
than 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 emitsmetadata.code.branch_hint): an objdump A/B on a
real Wado component shows the hints take effect. The harness and numbers are
published as a reproducible experiment:
<https://github.com/wado-lang/wado/pull/1152>.Scope and follow-ups
This is intentionally the codegen + knob slice. Tracked as follow-ups before
flipping the default on:
Validate the section and reject bad hints (the spec restricts hints to
br_if
andifonly).branch_hint.wastexercises bothassert_malformed_custom
(duplicate hint, hint outside a function) andassert_invalid_custom(hint
whose 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 ungating
the test (currently skipped incrates/test-util/src/wast.rs).
wasm-smithbranch-hint emission + fuzzing soak, then enable by default and
updatemodule_generation_uses_expected_proposals.The stability matrix is updated accordingly (Tier 3, API/C-API done, Tests/Fuzzed
pending, off by default).
gfx updated PR #13459.
gfx updated PR #13459.
gfx requested Copilot for a review on PR #13459.
:thumbs_up: alexcrichton submitted PR review:
Thanks!
alexcrichton added PR #13459 Implement the branch-hinting proposal (parse + Cranelift cold blocks) to the merge queue
:check: alexcrichton merged PR #13459.
alexcrichton removed PR #13459 Implement the branch-hinting proposal (parse + Cranelift cold blocks) from the merge queue
Last updated: Jun 01 2026 at 09:49 UTC