Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

JIT: Remove empty try regions #8949

Merged
merged 2 commits into from
Jan 27, 2017
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
40 changes: 38 additions & 2 deletions Documentation/design-docs/finally-optimizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ which is almost like a regular function with a prolog and epilog. A
custom calling convention gives the funclet access to the parent stack
frame.

In this proposal we outline two optimizations for finallys: removing
empty finallys and finally cloning.
In this proposal we outline three optimizations for finallys: removing
empty trys, removing empty finallys and finally cloning.

Empty Finally Removal
---------------------
Expand Down Expand Up @@ -175,6 +175,42 @@ G_M60484_IG06:
Empty finally removal is unconditionally profitable: it should always
reduce code size and improve code speed.

Empty Try Removal
---------------------

If the try region of a try-finally is empty, and the jitted code will
execute on a runtime that does not protect finally execution from
thread abort, then the try-finally can be replaced with just the
content of the finally.

Empty trys with non-empty finallys often exist in code that must run
under both thread-abort aware and non-thread-abort aware runtimes. In
the former case the placement of cleanup code in the finally ensures
that the cleanup code will execute fully. But if thread abort is not
possible, the extra protection offered by the finally is not needed.

Empty try removal looks for try-finallys where the try region does
nothing except invoke the finally. There are currently two different
EH implementation models, so the try screening has two cases:

* callfinally thunks (x64/arm64): the try must be a single empty
basic block that always jumps to a callfinally that is the first

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARM32 doesn't use thunks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will update to note x64/arm64 use callfinally thunks, x86/arm32 do not.

half of a callfinally/always pair;
* non-callfinally thunks (x86/arm32): the try must be a
callfinally/always pair where the first block is an empty callfinally.

The screening then verifies that the callfinally identified above is
the only callfinally for the try. No other callfinallys are expected
because this try cannot have multiple leaves and its handler cannot be
reached by nested exit paths.

When the empty try is identified, the jit modifies the
callfinally/always pair to branch to the handler, modifies the
handler's return to branch directly to the continuation (the
branch target of the second half of the callfinally/always pair),
updates various status flags on the blocks, and then removes the
try-finally region.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the "callfinally thunks" case, you'll have always->callfinally, converted to always->always. Can you just get rid of the thunk entirely and redirect this thunk always?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you certainly could do that.

I've been on the fence with these optimizations deciding how much cleanup they should do. On the one hand it seems entirely reasonable to let the subsequent flow opts clean things up, and leaving redundant and/or unreachable blocks around doesn't seem to cause trouble or inhibit other opts. On the other it does not take to much extra work to fix things here.

In the callfinally thunk case the only reference to the callfinally will be in the try, and we already know that the try is a single empty jump-always block. So it would be easy enough to retarget that instead.

That still leaves dead blocks around, and flow opts will almost certainly remove the try block itself later on...

Finally Cloning
---------------

Expand Down
4 changes: 4 additions & 0 deletions src/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -3503,10 +3503,14 @@ class Compiler

void fgInline();

void fgRemoveEmptyTry();

void fgRemoveEmptyFinally();

void fgCloneFinally();

void fgCleanupContinuation(BasicBlock* continuation);

GenTreePtr fgGetCritSectOfStaticMethod();

#if !defined(_TARGET_X86_)
Expand Down
1 change: 1 addition & 0 deletions src/jit/compphases.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ CompPhaseNameMacro(PHASE_POST_IMPORT, "Post-import",
CompPhaseNameMacro(PHASE_MORPH_INIT, "Morph - Init", "MOR-INIT" ,false, -1)
CompPhaseNameMacro(PHASE_MORPH_INLINE, "Morph - Inlining", "MOR-INL", false, -1)
CompPhaseNameMacro(PHASE_MORPH_IMPBYREF, "Morph - ByRefs", "MOR-BYREF",false, -1)
CompPhaseNameMacro(PHASE_EMPTY_TRY, "Remove empty try", "EMPTYTRY", false, -1)
CompPhaseNameMacro(PHASE_EMPTY_FINALLY, "Remove empty finally", "EMPTYFIN", false, -1)
CompPhaseNameMacro(PHASE_CLONE_FINALLY, "Clone finally", "CLONEFIN", false, -1)
CompPhaseNameMacro(PHASE_STR_ADRLCL, "Morph - Structs/AddrExp", "MOR-STRAL",false, -1)
Expand Down
Loading