-
Notifications
You must be signed in to change notification settings - Fork 835
[Branch Hinting] Add binary support #7572
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
Changes from all commits
a954eec
991058e
b14103c
1f6ed00
aef1dd7
57f19ef
1fe0ab5
5624819
f3c5257
4c645a6
5553c45
17c9fd1
6987c9a
9191093
0c11461
7429f12
57b9eec
deec0b8
78d5064
9011302
c47fcee
fa66aac
9225708
1cac4d7
87267fb
de1dd26
0a9ef92
0ab80b0
f99f04e
0cdcfda
c34222c
c7f5de2
d17c155
36caffb
f66d19c
d220c4e
0248a95
7b15774
1f227b4
1609738
c61b080
a6efd68
d659951
e9e4389
3a81bc0
d977d5f
396fc52
31c4823
5ccfabb
0e97014
f1d1ce7
b6d69f7
d8bde89
a7357ab
e337f5c
9a0535b
344d08c
4855925
d0e06b7
fe7fdfc
a5a8fff
057e2a1
ef58080
5cd9933
5396d00
b05e963
55c0eb9
d3a2e5a
18b61ef
4ddde5b
7c5a7ce
e652d11
1b41943
541c9ba
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 |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| #include "support/debug.h" | ||
| #include "support/stdckdint.h" | ||
| #include "support/string.h" | ||
| #include "wasm-annotations.h" | ||
| #include "wasm-binary.h" | ||
| #include "wasm-debug.h" | ||
| #include "wasm-limits.h" | ||
|
|
@@ -474,6 +475,21 @@ void WasmBinaryWriter::writeFunctions() { | |
| } | ||
| }); | ||
| finishSection(sectionStart); | ||
|
|
||
| // Code annotations must come before the code section (see comment on | ||
| // writeCodeAnnotations). | ||
| if (auto annotations = writeCodeAnnotations()) { | ||
| // We need to move the code section and put the annotations before it. | ||
| auto& annotationsBuffer = *annotations; | ||
| auto oldSize = o.size(); | ||
| o.resize(oldSize + annotationsBuffer.size()); | ||
|
|
||
| // |sectionStart| is the start of the contents of the section. Subtract 1 to | ||
| // include the section code as well, so we move all of it. | ||
| std::move_backward(&o[sectionStart - 1], &o[oldSize], o.end()); | ||
| std::copy( | ||
| annotationsBuffer.begin(), annotationsBuffer.end(), &o[sectionStart - 1]); | ||
| } | ||
| } | ||
|
|
||
| void WasmBinaryWriter::writeStrings() { | ||
|
|
@@ -1496,14 +1512,17 @@ void WasmBinaryWriter::trackExpressionStart(Expression* curr, Function* func) { | |
| // binary locations tracked, then track it in the output as well. We also | ||
| // track locations of instructions that have code annotations, as their binary | ||
| // location goes in the custom section. | ||
| if (func && !func->expressionLocations.empty()) { | ||
| if (func && (!func->expressionLocations.empty() || | ||
| func->codeAnnotations.count(curr))) { | ||
| binaryLocations.expressions[curr] = | ||
| BinaryLocations::Span{BinaryLocation(o.size()), 0}; | ||
| binaryLocationTrackedExpressionsForFunc.push_back(curr); | ||
| } | ||
| } | ||
|
|
||
| void WasmBinaryWriter::trackExpressionEnd(Expression* curr, Function* func) { | ||
| // TODO: If we need to track the end of annotated code locations, we need to | ||
| // enable that here. | ||
| if (func && !func->expressionLocations.empty()) { | ||
| auto& span = binaryLocations.expressions.at(curr); | ||
| span.end = o.size(); | ||
|
|
@@ -1513,11 +1532,123 @@ void WasmBinaryWriter::trackExpressionEnd(Expression* curr, Function* func) { | |
| void WasmBinaryWriter::trackExpressionDelimiter(Expression* curr, | ||
| Function* func, | ||
| size_t id) { | ||
| // TODO: If we need to track the delimiters of annotated code locations, we | ||
| // need to enable that here. | ||
| if (func && !func->expressionLocations.empty()) { | ||
| binaryLocations.delimiters[curr][id] = o.size(); | ||
| } | ||
| } | ||
|
|
||
| std::optional<BufferWithRandomAccess> WasmBinaryWriter::writeCodeAnnotations() { | ||
| // Assemble the info for Branch Hinting: for each function, a vector of the | ||
| // hints. | ||
| struct ExprHint { | ||
| Expression* expr; | ||
| // The offset we will write in the custom section. | ||
| BinaryLocation offset; | ||
| Function::CodeAnnotation* hint; | ||
| }; | ||
|
|
||
| struct FuncHints { | ||
| Name func; | ||
| std::vector<ExprHint> exprHints; | ||
| }; | ||
|
|
||
| std::vector<FuncHints> funcHintsVec; | ||
|
|
||
| for (auto& func : wasm->functions) { | ||
| // Collect the Branch Hints for this function. | ||
| FuncHints funcHints; | ||
|
|
||
| // We compute the location of the function declaration area (where the | ||
| // locals are declared) the first time we need it. | ||
| BinaryLocation funcDeclarationsOffset = 0; | ||
|
|
||
| for (auto& [expr, annotation] : func->codeAnnotations) { | ||
| if (annotation.branchLikely) { | ||
| auto exprIter = binaryLocations.expressions.find(expr); | ||
| if (exprIter == binaryLocations.expressions.end()) { | ||
| // No expression exists for this annotation - perhaps optimizations | ||
| // removed it. | ||
| continue; | ||
| } | ||
| auto exprOffset = exprIter->second.start; | ||
|
|
||
| if (!funcDeclarationsOffset) { | ||
| auto funcIter = binaryLocations.functions.find(func.get()); | ||
| assert(funcIter != binaryLocations.functions.end()); | ||
| funcDeclarationsOffset = funcIter->second.declarations; | ||
| } | ||
|
|
||
| // Compute the offset: it should be relative to the start of the | ||
| // function locals (i.e. the function declarations). | ||
| auto offset = exprOffset - funcDeclarationsOffset; | ||
|
|
||
| funcHints.exprHints.push_back(ExprHint{expr, offset, &annotation}); | ||
| } | ||
| } | ||
|
|
||
| if (funcHints.exprHints.empty()) { | ||
| continue; | ||
| } | ||
|
|
||
| // We found something. Finalize the data. | ||
| funcHints.func = func->name; | ||
|
|
||
| // Hints must be sorted by increasing binary offset. | ||
| std::sort( | ||
| funcHints.exprHints.begin(), | ||
| funcHints.exprHints.end(), | ||
| [](const ExprHint& a, const ExprHint& b) { return a.offset < b.offset; }); | ||
|
|
||
| funcHintsVec.emplace_back(std::move(funcHints)); | ||
| } | ||
|
|
||
| if (funcHintsVec.empty()) { | ||
| return {}; | ||
| } | ||
|
|
||
| if (sourceMap) { | ||
| // TODO: This mode may not matter (when debugging, code annotations are an | ||
| // optimization that can be skipped), but atm source maps cause | ||
| // annotations to break. | ||
|
Comment on lines
+1612
to
+1614
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. What's the reason for the breakage?
Member
Author
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. I'm not sure, I only noticed it by chance, and added the TODO to investigate.
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. Eventually I think we will need this to work. One use case for source maps is deobfuscation of stack traces collected from the wild, which means they need to be accurate (to the extent possible) for production binaries.
Member
Author
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. Ah, makes sense. I'll get to it sooner, then. |
||
| Fatal() << "Annotations are not supported with source maps"; | ||
| } | ||
|
|
||
| BufferWithRandomAccess buffer; | ||
|
|
||
| // We found data: emit the section. | ||
| buffer << uint8_t(BinaryConsts::Custom); | ||
| auto lebPos = buffer.writeU32LEBPlaceholder(); | ||
| buffer.writeInlineString(Annotations::BranchHint.str); | ||
|
|
||
| buffer << U32LEB(funcHintsVec.size()); | ||
| for (auto& funcHints : funcHintsVec) { | ||
| buffer << U32LEB(getFunctionIndex(funcHints.func)); | ||
|
|
||
| buffer << U32LEB(funcHints.exprHints.size()); | ||
| for (auto& exprHint : funcHints.exprHints) { | ||
| buffer << U32LEB(exprHint.offset); | ||
|
|
||
| // Hint size, always 1 for now. | ||
| buffer << U32LEB(1); | ||
|
|
||
| // We must only emit hints that are present. | ||
| assert(exprHint.hint->branchLikely); | ||
|
|
||
| // Hint contents: likely or not. | ||
| buffer << U32LEB(int(*exprHint.hint->branchLikely)); | ||
| } | ||
| } | ||
|
|
||
| // Write the final size. We can ignore the return value, which is the number | ||
| // of bytes we shrank (if the LEB was smaller than the maximum size), as no | ||
| // value in this section cares. | ||
| buffer.emitRetroactiveSectionSizeLEB(lebPos); | ||
|
|
||
| return buffer; | ||
| } | ||
|
|
||
| void WasmBinaryWriter::writeData(const char* data, size_t size) { | ||
| for (size_t i = 0; i < size; i++) { | ||
| o << int8_t(data[i]); | ||
|
|
@@ -1792,12 +1923,6 @@ WasmBinaryReader::WasmBinaryReader(Module& wasm, | |
| } | ||
|
|
||
| void WasmBinaryReader::preScan() { | ||
| // TODO: Once we support code annotations here, we will need to always scan, | ||
| // but for now, DWARF is the only reason. | ||
| if (!DWARF) { | ||
| return; | ||
| } | ||
|
|
||
| assert(pos == 0); | ||
| getInt32(); // magic | ||
| getInt32(); // version | ||
|
|
@@ -1813,12 +1938,25 @@ void WasmBinaryReader::preScan() { | |
| auto oldPos = pos; | ||
| if (sectionCode == BinaryConsts::Section::Custom) { | ||
| auto sectionName = getInlineString(); | ||
|
|
||
| // Code annotations require code locations. | ||
| // TODO: For Branch Hinting, we could note which functions require | ||
| // code locations, as an optimization. | ||
| if (sectionName == Annotations::BranchHint) { | ||
| needCodeLocations = true; | ||
| // Do not break, so we keep looking for DWARF. | ||
| } | ||
|
|
||
| // DWARF sections contain code offsets. | ||
| if (DWARF && Debug::isDWARFSection(sectionName)) { | ||
| needCodeLocations = true; | ||
| foundDWARF = true; | ||
| break; | ||
| } | ||
|
|
||
| // TODO: We could stop early if we see the Code section and DWARF is | ||
| // disabled, as BranchHint must appear first, but this seems to | ||
| // make practically no difference in practice. | ||
| } | ||
| pos = oldPos + payloadLen; | ||
| } | ||
|
|
@@ -1933,6 +2071,12 @@ void WasmBinaryReader::read() { | |
| } | ||
| } | ||
|
|
||
| // Go back and parse things we deferred. | ||
| if (branchHintsPos) { | ||
| pos = branchHintsPos; | ||
| readBranchHints(branchHintsLen); | ||
| } | ||
|
|
||
| validateBinary(); | ||
| } | ||
|
|
||
|
|
@@ -1953,6 +2097,10 @@ void WasmBinaryReader::readCustomSection(size_t payloadLen) { | |
| readDylink(payloadLen); | ||
| } else if (sectionName.equals(BinaryConsts::CustomSections::Dylink0)) { | ||
| readDylink0(payloadLen); | ||
| } else if (sectionName == Annotations::BranchHint) { | ||
| // Only note the position and length, we read this later. | ||
| branchHintsPos = pos; | ||
| branchHintsLen = payloadLen; | ||
| } else { | ||
| // an unfamiliar custom section | ||
| if (sectionName.equals(BinaryConsts::CustomSections::Linking)) { | ||
|
|
@@ -5100,6 +5248,62 @@ void WasmBinaryReader::readDylink0(size_t payloadLen) { | |
| } | ||
| } | ||
|
|
||
| void WasmBinaryReader::readBranchHints(size_t payloadLen) { | ||
| auto sectionPos = pos; | ||
|
|
||
| auto numFuncs = getU32LEB(); | ||
| for (Index i = 0; i < numFuncs; i++) { | ||
| auto funcIndex = getU32LEB(); | ||
| if (funcIndex >= wasm.functions.size()) { | ||
| throwError("bad BranchHint function"); | ||
| } | ||
|
|
||
| auto& func = wasm.functions[funcIndex]; | ||
|
|
||
| // The encoded offsets we read below are relative to the start of the | ||
| // function's locals (the declarations). | ||
| auto funcLocalsOffset = func->funcLocation.declarations; | ||
|
|
||
| // We have a map of expressions to their locations. Invert that to get the | ||
| // map we will use below, from offsets to expressions. | ||
| std::unordered_map<BinaryLocation, Expression*> locationsMap; | ||
|
|
||
| for (auto& [expr, span] : func->expressionLocations) { | ||
| locationsMap[span.start] = expr; | ||
| } | ||
|
|
||
| auto numHints = getU32LEB(); | ||
| for (Index hint = 0; hint < numHints; hint++) { | ||
| // To get the absolute offset, add the function's offset. | ||
| auto relativeOffset = getU32LEB(); | ||
| auto absoluteOffset = funcLocalsOffset + relativeOffset; | ||
|
|
||
| auto iter = locationsMap.find(absoluteOffset); | ||
| if (iter == locationsMap.end()) { | ||
| throwError("bad BranchHint offset"); | ||
| } | ||
| auto* expr = iter->second; | ||
|
|
||
| auto size = getU32LEB(); | ||
| if (size != 1) { | ||
| throwError("bad BranchHint size"); | ||
| } | ||
|
|
||
| auto likely = getU32LEB(); | ||
| if (likely != 0 && likely != 1) { | ||
| throwError("bad BranchHint value"); | ||
| } | ||
|
|
||
| // Apply the valid hint. | ||
| func->codeAnnotations[expr].branchLikely = likely; | ||
| } | ||
| } | ||
|
|
||
| if (pos != sectionPos + payloadLen) { | ||
| throwError("bad BranchHint section size"); | ||
| } | ||
| } | ||
|
|
||
| Index WasmBinaryReader::readMemoryAccess(Address& alignment, Address& offset) { | ||
| auto rawAlignment = getU32LEB(); | ||
| bool hasMemIdx = false; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| ;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. | ||
|
|
||
| ;; RUN: wasm-opt %s --monomorphize --roundtrip -all -S -o - | filecheck %s | ||
|
|
||
| ;; The callee, which we will monomorphize, has a branch hint. The hinted code | ||
| ;; will end up vanishing entirely, but not the function it is in, so we end up | ||
| ;; with an annotation without an instruction in the binary for it. We should | ||
| ;; ignore it and not error. | ||
| (module | ||
| ;; CHECK: (func $callee (type $1) (param $0 i32) | ||
| ;; CHECK-NEXT: (nop) | ||
| ;; CHECK-NEXT: ) | ||
| (func $callee (param $0 i32) | ||
| (block $block | ||
| (@metadata.code.branch_hint "\00") | ||
| (br_if $block | ||
| (i32.const 0) | ||
| ) | ||
| ) | ||
| ) | ||
|
|
||
| ;; CHECK: (func $caller (type $0) | ||
| ;; CHECK-NEXT: (call $callee_2) | ||
| ;; CHECK-NEXT: ) | ||
| (func $caller | ||
| (call $callee | ||
| (i32.const 0) | ||
| ) | ||
| ) | ||
| ) | ||
|
|
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.
It would be simpler if we wrote sections to several buffers and then concatenated them at the end rather than shifting contents around within buffers. That would be a bigger change, though.
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.
Yes, and possibly less efficient (though I'm not sure, maybe reallocating a single buffer is worse?)