Skip to content

Commit ea477c7

Browse files
authored
[Branch Hinting] Add useful passes to generate and log branch hints (#7695)
--instrument-branch-hints adds logging on every branch hint, which at runtime reports a unique ID, the branch hint prediction, and whether the branch was taken at runtime. This can be used to verify that branch hints are actually valid in practice. Also add --randomize-branch-hints, which just adds random hints in code, basically sets things up for fuzzing. Of course, branch hints may be inaccurate 50% of the time, but we can still fuzz and see if the inaccuracy was preserved.
1 parent 03c63e4 commit ea477c7

File tree

11 files changed

+1061
-0
lines changed

11 files changed

+1061
-0
lines changed

scripts/test/fuzzing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@
133133
'string-lifting-section.wast',
134134
# TODO: fuzzer support for uninhabitable imported globals
135135
'exact-references.wast',
136+
# We do not have full suppor for these imports in all parts of the fuzzer.
137+
'instrument-branch-hints.wast',
136138
]
137139

138140

src/passes/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ set(passes_SOURCES
5252
HeapStoreOptimization.cpp
5353
I64ToI32Lowering.cpp
5454
Inlining.cpp
55+
InstrumentBranchHints.cpp
5556
InstrumentLocals.cpp
5657
InstrumentMemory.cpp
5758
Intrinsics.cpp
@@ -102,6 +103,7 @@ set(passes_SOURCES
102103
Strip.cpp
103104
StripTargetFeatures.cpp
104105
TraceCalls.cpp
106+
RandomizeBranchHints.cpp
105107
RedundantSetElimination.cpp
106108
RemoveImports.cpp
107109
RemoveMemoryInit.cpp

src/passes/InstrumentBranchHints.cpp

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2025 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
//
18+
// Instruments branch hints and their targets, adding logging that allows us to
19+
// see if the hints were valid or not. We turn
20+
//
21+
// @metadata.branch.hint B
22+
// if (condition) {
23+
// X
24+
// } else {
25+
// Y
26+
// }
27+
//
28+
// into
29+
//
30+
// @metadata.branch.hint B
31+
// ;; log the ID of the condition (123), the prediction (B), and the actual
32+
// ;; runtime result (temp == condition).
33+
// if (temp = condition; log(123, B, temp); temp) {
34+
// X
35+
// } else {
36+
// Y
37+
// }
38+
//
39+
// Concretely, we emit calls to this logging function:
40+
//
41+
// (import "fuzzing-support" "log-branch"
42+
// (func $log-branch (param i32 i32 i32)) ;; ID, prediction, actual
43+
// )
44+
//
45+
// This can be used to verify that branch hints are accurate, by implementing
46+
// the import like this for example:
47+
//
48+
// imports['fuzzing-support']['log-branch'] = (id, prediction, actual) => {
49+
// // We only care about truthiness of the expected and actual values.
50+
// expected = +!!expected;
51+
// actual = +!!actual;
52+
// // Throw if the hint said this branch would be taken, but it was not, or
53+
// // vice versa.
54+
// if (expected != actual) throw `Bad branch hint! (${id})`;
55+
// };
56+
//
57+
58+
#include "ir/eh-utils.h"
59+
#include "ir/names.h"
60+
#include "ir/properties.h"
61+
#include "pass.h"
62+
#include "wasm-builder.h"
63+
#include "wasm.h"
64+
65+
namespace wasm {
66+
67+
namespace {
68+
69+
// The branch id, which increments as we go.
70+
int branchId = 1;
71+
72+
struct InstrumentBranchHints
73+
: public WalkerPass<PostWalker<InstrumentBranchHints>> {
74+
75+
using Super = WalkerPass<PostWalker<InstrumentBranchHints>>;
76+
77+
// The module and base names of our import.
78+
const Name MODULE = "fuzzing-support";
79+
const Name BASE = "log-branch";
80+
81+
// The internal name of our import.
82+
Name logBranch;
83+
84+
void visitIf(If* curr) { processCondition(curr); }
85+
86+
void visitBreak(Break* curr) {
87+
if (curr->condition) {
88+
processCondition(curr);
89+
}
90+
}
91+
92+
// TODO: BrOn, but the condition there is not an i32
93+
94+
bool addedInstrumentation = false;
95+
96+
template<typename T> void processCondition(T* curr) {
97+
if (curr->condition->type == Type::unreachable) {
98+
// This branch is not even reached.
99+
return;
100+
}
101+
102+
auto likely = getFunction()->codeAnnotations[curr].branchLikely;
103+
if (!likely) {
104+
return;
105+
}
106+
107+
Builder builder(*getModule());
108+
109+
// Pick an ID for this branch.
110+
int id = branchId++;
111+
112+
// Instrument the condition.
113+
auto tempLocal = builder.addVar(getFunction(), Type::i32);
114+
auto* set = builder.makeLocalSet(tempLocal, curr->condition);
115+
auto* idConst = builder.makeConst(Literal(int32_t(id)));
116+
auto* guess = builder.makeConst(Literal(int32_t(*likely)));
117+
auto* get1 = builder.makeLocalGet(tempLocal, Type::i32);
118+
auto* log = builder.makeCall(logBranch, {idConst, guess, get1}, Type::none);
119+
auto* get2 = builder.makeLocalGet(tempLocal, Type::i32);
120+
curr->condition = builder.makeBlock({set, log, get2});
121+
addedInstrumentation = true;
122+
}
123+
124+
void doWalkFunction(Function* func) {
125+
Super::doWalkFunction(func);
126+
127+
// Our added blocks may have caused nested pops.
128+
if (addedInstrumentation) {
129+
EHUtils::handleBlockNestedPops(func, *getModule());
130+
addedInstrumentation = false;
131+
}
132+
}
133+
134+
void doWalkModule(Module* module) {
135+
// Find our import, if we were already run on this module.
136+
for (auto& func : module->functions) {
137+
if (func->module == MODULE && func->base == BASE) {
138+
logBranch = func->name;
139+
break;
140+
}
141+
}
142+
// Otherwise, add it.
143+
if (!logBranch) {
144+
auto* func = module->addFunction(Builder::makeFunction(
145+
Names::getValidFunctionName(*module, BASE),
146+
Signature({Type::i32, Type::i32, Type::i32}, Type::none),
147+
{}));
148+
func->module = MODULE;
149+
func->base = BASE;
150+
logBranch = func->name;
151+
}
152+
153+
// Walk normally, using logBranch as we go.
154+
Super::doWalkModule(module);
155+
}
156+
};
157+
158+
} // anonymous namespace
159+
160+
Pass* createInstrumentBranchHintsPass() { return new InstrumentBranchHints(); }
161+
162+
} // namespace wasm

src/passes/RandomizeBranchHints.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2025 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
//
18+
// Apply random branch hints. This is really only useful for fuzzing. The
19+
// randomness here is deterministic, so that reducing can work.
20+
//
21+
22+
#include "pass.h"
23+
#include "support/hash.h"
24+
#include "wasm-builder.h"
25+
#include "wasm.h"
26+
27+
namespace wasm {
28+
29+
struct RandomizeBranchHints
30+
: public WalkerPass<
31+
PostWalker<RandomizeBranchHints,
32+
UnifiedExpressionVisitor<RandomizeBranchHints>>> {
33+
34+
uint64_t hash = 42;
35+
36+
void visitExpression(Expression* curr) {
37+
// Add some deterministic randomness as we go.
38+
deterministic_hash_combine(hash, curr->_id);
39+
}
40+
41+
void visitIf(If* curr) {
42+
deterministic_hash_combine(hash, 1337);
43+
processCondition(curr);
44+
}
45+
46+
void visitBreak(Break* curr) {
47+
deterministic_hash_combine(hash, 99999);
48+
if (curr->condition) {
49+
processCondition(curr);
50+
}
51+
}
52+
53+
template<typename T> void processCondition(T* curr) {
54+
auto& likely = getFunction()->codeAnnotations[curr].branchLikely;
55+
switch (hash % 3) {
56+
case 0:
57+
likely = true;
58+
break;
59+
case 1:
60+
likely = false;
61+
break;
62+
case 2:
63+
likely = {};
64+
break;
65+
}
66+
}
67+
};
68+
69+
Pass* createRandomizeBranchHintsPass() { return new RandomizeBranchHints(); }
70+
71+
} // namespace wasm

src/passes/pass.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ void PassRegistry::registerPasses() {
259259
"trace-calls",
260260
"instrument the build with code to intercept specific function calls",
261261
createTraceCallsPass);
262+
registerPass("instrument-branch-hints",
263+
"instrument branch hints so we can see which guessed right",
264+
createInstrumentBranchHintsPass);
262265
registerPass(
263266
"instrument-locals",
264267
"instrument the build with code to intercept all loads and stores",
@@ -409,6 +412,9 @@ void PassRegistry::registerPasses() {
409412
registerPass("propagate-globals-globally",
410413
"propagate global values to other globals (useful for tests)",
411414
createPropagateGlobalsGloballyPass);
415+
registerTestPass("randomize-branch-hints",
416+
"randomize branch hints (for fuzzing)",
417+
createRandomizeBranchHintsPass);
412418
registerPass("remove-non-js-ops",
413419
"removes operations incompatible with js",
414420
createRemoveNonJSOpsPass);

src/passes/passes.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Pass* createLocalSubtypingPass();
7979
Pass* createLogExecutionPass();
8080
Pass* createIntrinsicLoweringPass();
8181
Pass* createTraceCallsPass();
82+
Pass* createInstrumentBranchHintsPass();
8283
Pass* createInstrumentLocalsPass();
8384
Pass* createInstrumentMemoryPass();
8485
Pass* createLLVMMemoryCopyFillLoweringPass();
@@ -130,6 +131,7 @@ Pass* createPrintCallGraphPass();
130131
Pass* createPrintFeaturesPass();
131132
Pass* createPrintFunctionMapPass();
132133
Pass* createPropagateGlobalsGloballyPass();
134+
Pass* createRandomizeBranchHintsPass();
133135
Pass* createRemoveNonJSOpsPass();
134136
Pass* createRemoveImportsPass();
135137
Pass* createRemoveMemoryInitPass();

test/lit/help/wasm-metadce.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@
208208
;; CHECK-NEXT: --inlining-optimizing inline functions and optimizes
209209
;; CHECK-NEXT: where we inlined
210210
;; CHECK-NEXT:
211+
;; CHECK-NEXT: --instrument-branch-hints instrument branch hints so we
212+
;; CHECK-NEXT: can see which guessed right
213+
;; CHECK-NEXT:
211214
;; CHECK-NEXT: --instrument-locals instrument the build with code
212215
;; CHECK-NEXT: to intercept all loads and
213216
;; CHECK-NEXT: stores

test/lit/help/wasm-opt.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@
232232
;; CHECK-NEXT: --inlining-optimizing inline functions and optimizes
233233
;; CHECK-NEXT: where we inlined
234234
;; CHECK-NEXT:
235+
;; CHECK-NEXT: --instrument-branch-hints instrument branch hints so we
236+
;; CHECK-NEXT: can see which guessed right
237+
;; CHECK-NEXT:
235238
;; CHECK-NEXT: --instrument-locals instrument the build with code
236239
;; CHECK-NEXT: to intercept all loads and
237240
;; CHECK-NEXT: stores

test/lit/help/wasm2js.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@
172172
;; CHECK-NEXT: --inlining-optimizing inline functions and optimizes
173173
;; CHECK-NEXT: where we inlined
174174
;; CHECK-NEXT:
175+
;; CHECK-NEXT: --instrument-branch-hints instrument branch hints so we
176+
;; CHECK-NEXT: can see which guessed right
177+
;; CHECK-NEXT:
175178
;; CHECK-NEXT: --instrument-locals instrument the build with code
176179
;; CHECK-NEXT: to intercept all loads and
177180
;; CHECK-NEXT: stores

0 commit comments

Comments
 (0)