Skip to content

Commit 66211eb

Browse files
authored
[wasm] Implement the ENDFINALLY opcode in the jiterpreter (#84273)
* Mark the opcode following CALL_HANDLER interpreter opcodes as a back branch target * In the jiterpreter, record each CALL_HANDLER location when compiling them * Then when compiling an ENDFINALLY opcode check to see whether the branch target is one we recognize and if so do a branch, otherwise bail out * Tweak CFG to filter out branch targets that are never used * Add browser-bench measurement for try-finally
1 parent 6114f19 commit 66211eb

File tree

6 files changed

+114
-22
lines changed

6 files changed

+114
-22
lines changed

src/mono/mono/mini/interp/jiterpreter.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,10 @@ jiterp_should_abort_trace (InterpInst *ins, gboolean *inside_branch_block)
731731

732732
return TRACE_CONTINUE;
733733

734+
case MINT_ENDFINALLY:
735+
// May produce either a backwards branch or a bailout
736+
return TRACE_CONDITIONAL_ABORT;
737+
734738
case MINT_ICALL_V_P:
735739
case MINT_ICALL_V_V:
736740
case MINT_ICALL_P_P:
@@ -748,7 +752,6 @@ jiterp_should_abort_trace (InterpInst *ins, gboolean *inside_branch_block)
748752
case MINT_LEAVE_S_CHECK:
749753
return TRACE_ABORT;
750754

751-
case MINT_ENDFINALLY:
752755
case MINT_RETHROW:
753756
case MINT_PROF_EXIT:
754757
case MINT_PROF_EXIT_VOID:

src/mono/mono/mini/interp/transform.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8434,6 +8434,17 @@ generate_compacted_code (InterpMethod *rtm, TransformData *td)
84348434
if (ins->opcode == MINT_TIER_PATCHPOINT_DATA) {
84358435
int native_offset = (int)(ip - td->new_code);
84368436
patchpoint_data_index = add_patchpoint_data (td, patchpoint_data_index, native_offset, -ins->data [0]);
8437+
#if HOST_BROWSER
8438+
} else if (rtm->contains_traces && (
8439+
(ins->opcode == MINT_CALL_HANDLER_S) || (ins->opcode == MINT_CALL_HANDLER)
8440+
)) {
8441+
// While this formally isn't a backward branch target, we want to record
8442+
// the offset of its following instruction so that the jiterpreter knows
8443+
// to generate the necessary dispatch code to enable branching back to it.
8444+
ip = emit_compacted_instruction (td, ip, ins);
8445+
if (backward_branch_offsets_count < BACKWARD_BRANCH_OFFSETS_SIZE)
8446+
backward_branch_offsets[backward_branch_offsets_count++] = ip - td->new_code;
8447+
#endif
84378448
} else {
84388449
ip = emit_compacted_instruction (td, ip, ins);
84398450
}

src/mono/sample/wasm/browser-bench/Exceptions.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public ExceptionsTask()
2222
new TryCatchFilterInline(),
2323
new TryCatchFilterThrow(),
2424
new TryCatchFilterThrowApplies(),
25+
new TryFinally(),
2526
};
2627
}
2728

@@ -208,5 +209,26 @@ void DoThrow()
208209
throw new System.Exception("Reached DoThrow and threw");
209210
}
210211
}
212+
213+
class TryFinally : ExcMeasurement
214+
{
215+
public override string Name => "TryFinally";
216+
int j = 1;
217+
218+
public override void RunStep()
219+
{
220+
int i = 0;
221+
try
222+
{
223+
i += j;
224+
}
225+
finally
226+
{
227+
i += j;
228+
}
229+
if (i != 2)
230+
throw new System.Exception("Internal error");
231+
}
232+
}
211233
}
212234
}

src/mono/wasm/runtime/jiterpreter-support.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const enum BailoutReason {
5050
CallDelegate,
5151
Debugging,
5252
Icall,
53+
UnexpectedRetIp,
5354
}
5455

5556
export const BailoutReasonNames = [
@@ -78,6 +79,7 @@ export const BailoutReasonNames = [
7879
"CallDelegate",
7980
"Debugging",
8081
"Icall",
82+
"UnexpectedRetIp",
8183
];
8284

8385
type FunctionType = [
@@ -157,6 +159,7 @@ export class WasmBuilder {
157159
options!: JiterpreterOptions;
158160
constantSlots: Array<number> = [];
159161
backBranchOffsets: Array<MintOpcodePtr> = [];
162+
callHandlerReturnAddresses: Array<MintOpcodePtr> = [];
160163
nextConstantSlot = 0;
161164

162165
compressImportNames = false;
@@ -202,6 +205,7 @@ export class WasmBuilder {
202205
for (let i = 0; i < this.constantSlots.length; i++)
203206
this.constantSlots[i] = 0;
204207
this.backBranchOffsets.length = 0;
208+
this.callHandlerReturnAddresses.length = 0;
205209

206210
this.allowNullCheckOptimization = this.options.eliminateNullChecks;
207211
}
@@ -1009,6 +1013,7 @@ class Cfg {
10091013
blockStack: Array<MintOpcodePtr> = [];
10101014
backDispatchOffsets: Array<MintOpcodePtr> = [];
10111015
dispatchTable = new Map<MintOpcodePtr, number>();
1016+
observedBranchTargets = new Set<MintOpcodePtr>();
10121017
trace = 0;
10131018

10141019
constructor (builder: WasmBuilder) {
@@ -1025,6 +1030,7 @@ class Cfg {
10251030
this.lastSegmentEnd = 0;
10261031
this.overheadBytes = 10; // epilogue
10271032
this.dispatchTable.clear();
1033+
this.observedBranchTargets.clear();
10281034
this.trace = trace;
10291035
this.backDispatchOffsets.length = 0;
10301036
}
@@ -1071,6 +1077,7 @@ class Cfg {
10711077
}
10721078

10731079
branch (target: MintOpcodePtr, isBackward: boolean, isConditional: boolean) {
1080+
this.observedBranchTargets.add(target);
10741081
this.appendBlob();
10751082
this.segments.push({
10761083
type: "branch",
@@ -1140,13 +1147,19 @@ class Cfg {
11401147
this.backDispatchOffsets.length = 0;
11411148
// First scan the back branch target table and union it with the block stack
11421149
// This filters down to back branch targets that are reachable inside this trace
1150+
// Further filter it down by only including targets we have observed a branch to
1151+
// this helps for cases where the branch opcodes targeting the location were not
1152+
// compiled due to an abort or some other reason
11431153
for (let i = 0; i < this.backBranchTargets.length; i++) {
11441154
const offset = (this.backBranchTargets[i] * 2) + <any>this.startOfBody;
11451155
const breakDepth = this.blockStack.indexOf(offset);
1146-
if (breakDepth >= 0) {
1147-
this.dispatchTable.set(offset, this.backDispatchOffsets.length + 1);
1148-
this.backDispatchOffsets.push(offset);
1149-
}
1156+
if (breakDepth < 0)
1157+
continue;
1158+
if (!this.observedBranchTargets.has(offset))
1159+
continue;
1160+
1161+
this.dispatchTable.set(offset, this.backDispatchOffsets.length + 1);
1162+
this.backDispatchOffsets.push(offset);
11501163
}
11511164

11521165
if (this.backDispatchOffsets.length === 0) {

src/mono/wasm/runtime/jiterpreter-trace-generator.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
traceEip, nullCheckValidation,
2828
abortAtJittedLoopBodies, traceNullCheckOptimizations,
2929
nullCheckCaching, traceBackBranches,
30+
maxCallHandlerReturnAddresses,
3031

3132
mostRecentOptions,
3233

@@ -345,12 +346,13 @@ export function generateWasmBody (
345346
case MintOpcode.MINT_CALL_HANDLER_S:
346347
if (!emit_branch(builder, ip, frame, opcode))
347348
ip = abort;
348-
else
349+
else {
349350
// Technically incorrect, but the instructions following this one may not be executed
350351
// since we might have skipped over them.
351352
// FIXME: Identify when we should actually set the conditionally executed flag, perhaps
352353
// by doing a simple static flow analysis based on the displacements. Update heuristic too!
353354
isConditionallyExecuted = true;
355+
}
354356
break;
355357

356358
case MintOpcode.MINT_CKNULL: {
@@ -923,13 +925,41 @@ export function generateWasmBody (
923925
isLowValueOpcode = true;
924926
break;
925927

926-
case MintOpcode.MINT_ENDFINALLY:
927-
// This one might make sense to partially implement, but the jump target
928-
// is computed at runtime which would make it hard to figure out where
929-
// we need to put branch targets. Not worth just doing a conditional
930-
// bailout since finally blocks always run
931-
ip = abort;
928+
case MintOpcode.MINT_ENDFINALLY: {
929+
if (
930+
(builder.callHandlerReturnAddresses.length > 0) &&
931+
(builder.callHandlerReturnAddresses.length <= maxCallHandlerReturnAddresses)
932+
) {
933+
// console.log(`endfinally @0x${(<any>ip).toString(16)}. return addresses:`, builder.callHandlerReturnAddresses.map(ra => (<any>ra).toString(16)));
934+
// FIXME: Clean this codegen up
935+
// Load ret_ip
936+
const clauseIndex = getArgU16(ip, 1),
937+
clauseDataOffset = get_imethod_clause_data_offset(frame, clauseIndex);
938+
builder.local("pLocals");
939+
builder.appendU8(WasmOpcode.i32_load);
940+
builder.appendMemarg(clauseDataOffset, 0);
941+
// Stash it in a variable because we're going to need to use it multiple times
942+
builder.local("math_lhs32", WasmOpcode.set_local);
943+
// Do a bunch of trivial comparisons to see if ret_ip is one of our expected return addresses,
944+
// and if it is, generate a branch back to the dispatcher at the top
945+
for (let r = 0; r < builder.callHandlerReturnAddresses.length; r++) {
946+
const ra = builder.callHandlerReturnAddresses[r];
947+
builder.local("math_lhs32");
948+
builder.ptr_const(ra);
949+
builder.appendU8(WasmOpcode.i32_eq);
950+
builder.block(WasmValtype.void, WasmOpcode.if_);
951+
builder.cfg.branch(ra, ra < ip, true);
952+
builder.endBlock();
953+
}
954+
// If none of the comparisons succeeded we won't have branched anywhere, so bail out
955+
// This shouldn't happen during non-exception-handling execution unless the trace doesn't
956+
// contain the CALL_HANDLER that led here
957+
append_bailout(builder, ip, BailoutReason.UnexpectedRetIp);
958+
} else {
959+
ip = abort;
960+
}
932961
break;
962+
}
933963

934964
case MintOpcode.MINT_RETHROW:
935965
case MintOpcode.MINT_PROF_EXIT:
@@ -2444,7 +2474,8 @@ function append_call_handler_store_ret_ip (
24442474
builder.appendU8(WasmOpcode.i32_store);
24452475
builder.appendMemarg(clauseDataOffset, 0); // FIXME: 32-bit alignment?
24462476

2447-
// console.log(`call_handler clauseDataOffset=0x${clauseDataOffset.toString(16)} retIp=0x${retIp.toString(16)}`);
2477+
// console.log(`call_handler @0x${(<any>ip).toString(16)} retIp=0x${retIp.toString(16)}`);
2478+
builder.callHandlerReturnAddresses.push(retIp);
24482479
}
24492480

24502481
function emit_branch (
@@ -2496,10 +2527,14 @@ function emit_branch (
24962527
counters.backBranchesEmitted++;
24972528
return true;
24982529
} else {
2499-
if ((traceBackBranches > 0) || (builder.cfg.trace > 0))
2500-
console.log(`back branch target 0x${destination.toString(16)} not found in list ` +
2530+
if (destination < builder.cfg.entryIp) {
2531+
if ((traceBackBranches > 1) || (builder.cfg.trace > 1))
2532+
console.log(`${info[0]} target 0x${destination.toString(16)} before start of trace`);
2533+
} else if ((traceBackBranches > 0) || (builder.cfg.trace > 0))
2534+
console.log(`0x${(<any>ip).toString(16)} ${info[0]} target 0x${destination.toString(16)} not found in list ` +
25012535
builder.backBranchOffsets.map(bbo => "0x" + (<any>bbo).toString(16)).join(", ")
25022536
);
2537+
25032538
cwraps.mono_jiterp_boost_back_branch_target(destination);
25042539
// FIXME: Should there be a safepoint here?
25052540
append_bailout(builder, destination, BailoutReason.BackwardBranch);
@@ -2586,8 +2621,11 @@ function emit_branch (
25862621
builder.cfg.branch(destination, true, true);
25872622
counters.backBranchesEmitted++;
25882623
} else {
2589-
if ((traceBackBranches > 0) || (builder.cfg.trace > 0))
2590-
console.log(`back branch target 0x${destination.toString(16)} not found in list ` +
2624+
if (destination < builder.cfg.entryIp) {
2625+
if ((traceBackBranches > 1) || (builder.cfg.trace > 1))
2626+
console.log(`${info[0]} target 0x${destination.toString(16)} before start of trace`);
2627+
} else if ((traceBackBranches > 0) || (builder.cfg.trace > 0))
2628+
console.log(`0x${(<any>ip).toString(16)} ${info[0]} target 0x${destination.toString(16)} not found in list ` +
25912629
builder.backBranchOffsets.map(bbo => "0x" + (<any>bbo).toString(16)).join(", ")
25922630
);
25932631
// We didn't find a loop to branch to, so bail out

src/mono/wasm/runtime/jiterpreter.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ export const
6363
// Unproductive if we have backward branches enabled because it can stop us from jitting
6464
// nested loops
6565
abortAtJittedLoopBodies = true,
66+
// Enable generating conditional backward branches for ENDFINALLY opcodes if we saw some CALL_HANDLER
67+
// opcodes previously, up to this many potential return addresses. If a trace contains more potential
68+
// return addresses than this we will not emit code for the ENDFINALLY opcode
69+
maxCallHandlerReturnAddresses = 3,
70+
// Controls how many individual items (traces, bailouts, etc) are shown in the breakdown
71+
// at the end of a run when stats are enabled. The N highest ranking items will be shown.
72+
summaryStatCount = 30,
6673
// Emit a wasm nop between each managed interpreter opcode
6774
emitPadding = false,
6875
// Generate compressed names for imports so that modules have more space for code
@@ -984,7 +991,7 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) {
984991
console.log(`// traces bailed out ${bailoutCount} time(s) due to ${BailoutReasonNames[i]}`);
985992
}
986993

987-
for (let i = 0, c = 0; i < traces.length && c < 30; i++) {
994+
for (let i = 0, c = 0; i < traces.length && c < summaryStatCount; i++) {
988995
const trace = traces[i];
989996
if (!trace.bailoutCount)
990997
continue;
@@ -1016,7 +1023,7 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) {
10161023
console.log("// hottest call targets:");
10171024
const targetPointers = Object.keys(callTargetCounts);
10181025
targetPointers.sort((l, r) => callTargetCounts[Number(r)] - callTargetCounts[Number(l)]);
1019-
for (let i = 0, c = Math.min(20, targetPointers.length); i < c; i++) {
1026+
for (let i = 0, c = Math.min(summaryStatCount, targetPointers.length); i < c; i++) {
10201027
const targetMethod = Number(targetPointers[i]) | 0;
10211028
const pMethodName = cwraps.mono_wasm_method_get_full_name(<any>targetMethod);
10221029
const targetMethodName = Module.UTF8ToString(pMethodName);
@@ -1028,7 +1035,7 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) {
10281035

10291036
traces.sort((l, r) => r.hitCount - l.hitCount);
10301037
console.log("// hottest failed traces:");
1031-
for (let i = 0, c = 0; i < traces.length && c < 20; i++) {
1038+
for (let i = 0, c = 0; i < traces.length && c < summaryStatCount; i++) {
10321039
// this means the trace has a low hit count and we don't know its identity. no value in
10331040
// logging it.
10341041
if (!traces[i].name)
@@ -1064,15 +1071,13 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) {
10641071
case "newobj_slow":
10651072
case "switch":
10661073
case "rethrow":
1067-
case "endfinally":
10681074
case "end-of-body":
10691075
case "ret":
10701076
continue;
10711077

10721078
// not worth implementing / too difficult
10731079
case "intrins_marvin_block":
10741080
case "intrins_ascii_chars_to_uppercase":
1075-
case "newarr":
10761081
continue;
10771082
}
10781083
}

0 commit comments

Comments
 (0)