Skip to content
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

[wasm] Implement the ENDFINALLY opcode in the jiterpreter #84273

Merged
merged 4 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/mono/mono/mini/interp/jiterpreter.c
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,10 @@ jiterp_should_abort_trace (InterpInst *ins, gboolean *inside_branch_block)

return TRACE_CONTINUE;

case MINT_ENDFINALLY:
// May produce either a backwards branch or a bailout
return TRACE_CONDITIONAL_ABORT;

case MINT_ICALL_V_P:
case MINT_ICALL_V_V:
case MINT_ICALL_P_P:
Expand All @@ -748,7 +752,6 @@ jiterp_should_abort_trace (InterpInst *ins, gboolean *inside_branch_block)
case MINT_LEAVE_S_CHECK:
return TRACE_ABORT;

case MINT_ENDFINALLY:
case MINT_RETHROW:
case MINT_PROF_EXIT:
case MINT_PROF_EXIT_VOID:
Expand Down
11 changes: 11 additions & 0 deletions src/mono/mono/mini/interp/transform.c
Original file line number Diff line number Diff line change
Expand Up @@ -8434,6 +8434,17 @@ generate_compacted_code (InterpMethod *rtm, TransformData *td)
if (ins->opcode == MINT_TIER_PATCHPOINT_DATA) {
int native_offset = (int)(ip - td->new_code);
patchpoint_data_index = add_patchpoint_data (td, patchpoint_data_index, native_offset, -ins->data [0]);
#if HOST_BROWSER
} else if (rtm->contains_traces && (
(ins->opcode == MINT_CALL_HANDLER_S) || (ins->opcode == MINT_CALL_HANDLER)
)) {
// While this formally isn't a backward branch target, we want to record
// the offset of its following instruction so that the jiterpreter knows
// to generate the necessary dispatch code to enable branching back to it.
ip = emit_compacted_instruction (td, ip, ins);
if (backward_branch_offsets_count < BACKWARD_BRANCH_OFFSETS_SIZE)
backward_branch_offsets[backward_branch_offsets_count++] = ip - td->new_code;
#endif
} else {
ip = emit_compacted_instruction (td, ip, ins);
}
Expand Down
22 changes: 22 additions & 0 deletions src/mono/sample/wasm/browser-bench/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public ExceptionsTask()
new TryCatchFilterInline(),
new TryCatchFilterThrow(),
new TryCatchFilterThrowApplies(),
new TryFinally(),
};
}

Expand Down Expand Up @@ -208,5 +209,26 @@ void DoThrow()
throw new System.Exception("Reached DoThrow and threw");
}
}

class TryFinally : ExcMeasurement
{
public override string Name => "TryFinally";
int j = 1;

public override void RunStep()
{
int i = 0;
try
{
i += j;
}
finally
{
i += j;
}
if (i != 2)
throw new System.Exception("Internal error");
}
}
}
}
21 changes: 17 additions & 4 deletions src/mono/wasm/runtime/jiterpreter-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const enum BailoutReason {
CallDelegate,
Debugging,
Icall,
UnexpectedRetIp,
}

export const BailoutReasonNames = [
Expand Down Expand Up @@ -78,6 +79,7 @@ export const BailoutReasonNames = [
"CallDelegate",
"Debugging",
"Icall",
"UnexpectedRetIp",
];

type FunctionType = [
Expand Down Expand Up @@ -157,6 +159,7 @@ export class WasmBuilder {
options!: JiterpreterOptions;
constantSlots: Array<number> = [];
backBranchOffsets: Array<MintOpcodePtr> = [];
callHandlerReturnAddresses: Array<MintOpcodePtr> = [];
nextConstantSlot = 0;

compressImportNames = false;
Expand Down Expand Up @@ -202,6 +205,7 @@ export class WasmBuilder {
for (let i = 0; i < this.constantSlots.length; i++)
this.constantSlots[i] = 0;
this.backBranchOffsets.length = 0;
this.callHandlerReturnAddresses.length = 0;

this.allowNullCheckOptimization = this.options.eliminateNullChecks;
}
Expand Down Expand Up @@ -1009,6 +1013,7 @@ class Cfg {
blockStack: Array<MintOpcodePtr> = [];
backDispatchOffsets: Array<MintOpcodePtr> = [];
dispatchTable = new Map<MintOpcodePtr, number>();
observedBranchTargets = new Set<MintOpcodePtr>();
trace = 0;

constructor (builder: WasmBuilder) {
Expand All @@ -1025,6 +1030,7 @@ class Cfg {
this.lastSegmentEnd = 0;
this.overheadBytes = 10; // epilogue
this.dispatchTable.clear();
this.observedBranchTargets.clear();
this.trace = trace;
this.backDispatchOffsets.length = 0;
}
Expand Down Expand Up @@ -1071,6 +1077,7 @@ class Cfg {
}

branch (target: MintOpcodePtr, isBackward: boolean, isConditional: boolean) {
this.observedBranchTargets.add(target);
this.appendBlob();
this.segments.push({
type: "branch",
Expand Down Expand Up @@ -1140,13 +1147,19 @@ class Cfg {
this.backDispatchOffsets.length = 0;
// First scan the back branch target table and union it with the block stack
// This filters down to back branch targets that are reachable inside this trace
// Further filter it down by only including targets we have observed a branch to
// this helps for cases where the branch opcodes targeting the location were not
// compiled due to an abort or some other reason
for (let i = 0; i < this.backBranchTargets.length; i++) {
const offset = (this.backBranchTargets[i] * 2) + <any>this.startOfBody;
const breakDepth = this.blockStack.indexOf(offset);
if (breakDepth >= 0) {
this.dispatchTable.set(offset, this.backDispatchOffsets.length + 1);
this.backDispatchOffsets.push(offset);
}
if (breakDepth < 0)
continue;
if (!this.observedBranchTargets.has(offset))
continue;

this.dispatchTable.set(offset, this.backDispatchOffsets.length + 1);
this.backDispatchOffsets.push(offset);
}

if (this.backDispatchOffsets.length === 0) {
Expand Down
62 changes: 50 additions & 12 deletions src/mono/wasm/runtime/jiterpreter-trace-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
traceEip, nullCheckValidation,
abortAtJittedLoopBodies, traceNullCheckOptimizations,
nullCheckCaching, traceBackBranches,
maxCallHandlerReturnAddresses,

mostRecentOptions,

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

case MintOpcode.MINT_CKNULL: {
Expand Down Expand Up @@ -923,13 +925,41 @@ export function generateWasmBody (
isLowValueOpcode = true;
break;

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

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

// console.log(`call_handler clauseDataOffset=0x${clauseDataOffset.toString(16)} retIp=0x${retIp.toString(16)}`);
// console.log(`call_handler @0x${(<any>ip).toString(16)} retIp=0x${retIp.toString(16)}`);
builder.callHandlerReturnAddresses.push(retIp);
}

function emit_branch (
Expand Down Expand Up @@ -2496,10 +2527,14 @@ function emit_branch (
counters.backBranchesEmitted++;
return true;
} else {
if ((traceBackBranches > 0) || (builder.cfg.trace > 0))
console.log(`back branch target 0x${destination.toString(16)} not found in list ` +
if (destination < builder.cfg.entryIp) {
if ((traceBackBranches > 1) || (builder.cfg.trace > 1))
console.log(`${info[0]} target 0x${destination.toString(16)} before start of trace`);
} else if ((traceBackBranches > 0) || (builder.cfg.trace > 0))
console.log(`0x${(<any>ip).toString(16)} ${info[0]} target 0x${destination.toString(16)} not found in list ` +
builder.backBranchOffsets.map(bbo => "0x" + (<any>bbo).toString(16)).join(", ")
);

cwraps.mono_jiterp_boost_back_branch_target(destination);
// FIXME: Should there be a safepoint here?
append_bailout(builder, destination, BailoutReason.BackwardBranch);
Expand Down Expand Up @@ -2586,8 +2621,11 @@ function emit_branch (
builder.cfg.branch(destination, true, true);
counters.backBranchesEmitted++;
} else {
if ((traceBackBranches > 0) || (builder.cfg.trace > 0))
console.log(`back branch target 0x${destination.toString(16)} not found in list ` +
if (destination < builder.cfg.entryIp) {
if ((traceBackBranches > 1) || (builder.cfg.trace > 1))
console.log(`${info[0]} target 0x${destination.toString(16)} before start of trace`);
} else if ((traceBackBranches > 0) || (builder.cfg.trace > 0))
console.log(`0x${(<any>ip).toString(16)} ${info[0]} target 0x${destination.toString(16)} not found in list ` +
builder.backBranchOffsets.map(bbo => "0x" + (<any>bbo).toString(16)).join(", ")
);
// We didn't find a loop to branch to, so bail out
Expand Down
15 changes: 10 additions & 5 deletions src/mono/wasm/runtime/jiterpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export const
// Unproductive if we have backward branches enabled because it can stop us from jitting
// nested loops
abortAtJittedLoopBodies = true,
// Enable generating conditional backward branches for ENDFINALLY opcodes if we saw some CALL_HANDLER
// opcodes previously, up to this many potential return addresses. If a trace contains more potential
// return addresses than this we will not emit code for the ENDFINALLY opcode
maxCallHandlerReturnAddresses = 3,
// Controls how many individual items (traces, bailouts, etc) are shown in the breakdown
// at the end of a run when stats are enabled. The N highest ranking items will be shown.
summaryStatCount = 30,
// Emit a wasm nop between each managed interpreter opcode
emitPadding = false,
// Generate compressed names for imports so that modules have more space for code
Expand Down Expand Up @@ -984,7 +991,7 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) {
console.log(`// traces bailed out ${bailoutCount} time(s) due to ${BailoutReasonNames[i]}`);
}

for (let i = 0, c = 0; i < traces.length && c < 30; i++) {
for (let i = 0, c = 0; i < traces.length && c < summaryStatCount; i++) {
const trace = traces[i];
if (!trace.bailoutCount)
continue;
Expand Down Expand Up @@ -1016,7 +1023,7 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) {
console.log("// hottest call targets:");
const targetPointers = Object.keys(callTargetCounts);
targetPointers.sort((l, r) => callTargetCounts[Number(r)] - callTargetCounts[Number(l)]);
for (let i = 0, c = Math.min(20, targetPointers.length); i < c; i++) {
for (let i = 0, c = Math.min(summaryStatCount, targetPointers.length); i < c; i++) {
const targetMethod = Number(targetPointers[i]) | 0;
const pMethodName = cwraps.mono_wasm_method_get_full_name(<any>targetMethod);
const targetMethodName = Module.UTF8ToString(pMethodName);
Expand All @@ -1028,7 +1035,7 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) {

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

// not worth implementing / too difficult
case "intrins_marvin_block":
case "intrins_ascii_chars_to_uppercase":
case "newarr":
continue;
}
}
Expand Down