1
0
Fork 0
mirror of https://github.com/VSadov/Satori.git synced 2025-06-11 10:18:21 +09:00

JIT: Remove empty try regions (dotnet/coreclr#8949)

In runtimes that do not support thread abort, a try-finally with
an empty try can be optimized to simply the content of the finally.

See documentation update for more details.

Closes dotnet/coreclr#8924.


Commit migrated from 119b04c1d3
This commit is contained in:
Andy Ayers 2017-01-27 09:36:09 -08:00 committed by GitHub
parent 9916a38125
commit ac42a80586
6 changed files with 403 additions and 59 deletions

View file

@ -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
---------------------
@ -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
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.
Finally Cloning
---------------

View file

@ -3495,10 +3495,14 @@ public:
void fgInline();
void fgRemoveEmptyTry();
void fgRemoveEmptyFinally();
void fgCloneFinally();
void fgCleanupContinuation(BasicBlock* continuation);
GenTreePtr fgGetCritSectOfStaticMethod();
#if !defined(_TARGET_X86_)

View file

@ -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)

View file

@ -22635,33 +22635,8 @@ void Compiler::fgRemoveEmptyFinally()
leaveBlock->bbFlags &= ~BBF_KEEP_BBJ_ALWAYS;
fgRemoveBlock(leaveBlock, true);
// The postTryFinallyBlock may be a finalStep block.
// It is now a normal block, so clear the special keep
// always flag.
postTryFinallyBlock->bbFlags &= ~BBF_KEEP_BBJ_ALWAYS;
#if FEATURE_EH_FUNCLETS && defined(_TARGET_ARM_)
// Also, clear the finally target bit for arm
fgClearFinallyTargetBit(postTryFinallyBlock);
#endif // FEATURE_EH_FUNCLETS && defined(_TARGET_ARM_)
#if !FEATURE_EH_FUNCLETS
// Remove the GT_END_LFIN from the post-try-finally block.
// remove it since there is no finally anymore. Note we only
// expect to see one statement.
bool foundEndLFin = false;
for (GenTreeStmt* stmt = postTryFinallyBlock->firstStmt(); stmt != nullptr; stmt = stmt->gtNextStmt)
{
GenTreePtr expr = stmt->gtStmtExpr;
if (expr->gtOper == GT_END_LFIN)
{
assert(!foundEndLFin);
fgRemoveStmt(postTryFinallyBlock, stmt);
foundEndLFin = true;
}
}
assert(foundEndLFin);
#endif // !FEATURE_EH_FUNCLETS
// Cleanup the postTryFinallyBlock
fgCleanupContinuation(postTryFinallyBlock);
// Make sure iteration isn't going off the deep end.
assert(leaveBlock != endCallFinallyRangeBlock);
@ -22745,6 +22720,313 @@ void Compiler::fgRemoveEmptyFinally()
}
}
//------------------------------------------------------------------------
// fgRemoveEmptyTry: Optimize try/finallys where the try is empty
//
// Notes:
// In runtimes where thread abort is not possible, `try {} finally {S}`
// can be optimized to simply `S`. This method looks for such
// cases and removes the try-finally from the EH table, making
// suitable flow, block flag, statement, and region updates.
//
// This optimization is not legal in runtimes that support thread
// abort because those runtimes ensure that a finally is completely
// executed before continuing to process the thread abort. With
// this optimization, the code block `S` can lose special
// within-finally status and so complete execution is no longer
// guaranteed.
void Compiler::fgRemoveEmptyTry()
{
JITDUMP("\n*************** In fgRemoveEmptyTry()\n");
#ifdef FEATURE_CORECLR
bool enableRemoveEmptyTry = true;
#else
// Code in a finally gets special treatment in the presence of
// thread abort.
bool enableRemoveEmptyTry = false;
#endif // FEATURE_CORECLR
#ifdef DEBUG
// Allow override to enable/disable.
enableRemoveEmptyTry = (JitConfig.JitEnableRemoveEmptyTry() == 1);
#endif // DEBUG
if (!enableRemoveEmptyTry)
{
JITDUMP("Empty try removal disabled.\n");
return;
}
if (compHndBBtabCount == 0)
{
JITDUMP("No EH in this method, nothing to remove.\n");
return;
}
if (opts.MinOpts())
{
JITDUMP("Method compiled with minOpts, no removal.\n");
return;
}
if (opts.compDbgCode)
{
JITDUMP("Method compiled with debug codegen, no removal.\n");
return;
}
#ifdef DEBUG
if (verbose)
{
printf("\n*************** Before fgRemoveEmptyTry()\n");
fgDispBasicBlocks();
fgDispHandlerTab();
printf("\n");
}
#endif // DEBUG
// Look for try-finallys where the try is empty.
unsigned emptyCount = 0;
unsigned XTnum = 0;
while (XTnum < compHndBBtabCount)
{
EHblkDsc* const HBtab = &compHndBBtab[XTnum];
// Check if this is a try/finally. We could also look for empty
// try/fault but presumably those are rare.
if (!HBtab->HasFinallyHandler())
{
JITDUMP("EH#%u is not a try-finally; skipping.\n", XTnum);
XTnum++;
continue;
}
// Examine the try region
BasicBlock* const firstTryBlock = HBtab->ebdTryBeg;
BasicBlock* const lastTryBlock = HBtab->ebdTryLast;
BasicBlock* const firstHandlerBlock = HBtab->ebdHndBeg;
BasicBlock* const lastHandlerBlock = HBtab->ebdHndLast;
BasicBlock* const endHandlerBlock = lastHandlerBlock->bbNext;
assert(firstTryBlock->getTryIndex() == XTnum);
// Limit for now to trys that contain only a callfinally pair
// or branch to same.
if (!firstTryBlock->isEmpty())
{
JITDUMP("EH#%u first try block BB%02u not empty; skipping.\n", XTnum, firstTryBlock->bbNum);
XTnum++;
continue;
}
#if FEATURE_EH_CALLFINALLY_THUNKS
// Look for blocks that are always jumps to a call finally
// pair that targets the finally
if (firstTryBlock->bbJumpKind != BBJ_ALWAYS)
{
JITDUMP("EH#%u first try block BB%02u not jump to a callfinally; skipping.\n", XTnum, firstTryBlock->bbNum);
XTnum++;
continue;
}
BasicBlock* const callFinally = firstTryBlock->bbJumpDest;
// Look for call always pair. Note this will also disqualify
// empty try removal in cases where the finally doesn't
// return.
if (!callFinally->isBBCallAlwaysPair() || (callFinally->bbJumpDest != firstHandlerBlock))
{
JITDUMP("EH#%u first try block BB%02u always jumps but not to a callfinally; skipping.\n", XTnum,
firstTryBlock->bbNum);
XTnum++;
continue;
}
// Try itself must be a single block.
if (firstTryBlock != lastTryBlock)
{
JITDUMP("EH#%u first try block BB%02u not only block in try; skipping.\n", XTnum,
firstTryBlock->bbNext->bbNum);
XTnum++;
continue;
}
#else
// Look for call always pair within the try itself. Note this
// will also disqualify empty try removal in cases where the
// finally doesn't return.
if (!firstTryBlock->isBBCallAlwaysPair() || (firstTryBlock->bbJumpDest != firstHandlerBlock))
{
JITDUMP("EH#%u first try block BB%02u not a callfinally; skipping.\n", XTnum, firstTryBlock->bbNum);
XTnum++;
continue;
}
BasicBlock* const callFinally = firstTryBlock;
// Try must be a callalways pair of blocks.
if (firstTryBlock->bbNext != lastTryBlock)
{
JITDUMP("EH#%u block BB%02u not last block in try; skipping.\n", XTnum, firstTryBlock->bbNext->bbNum);
XTnum++;
continue;
}
#endif // FEATURE_EH_CALLFINALLY_THUNKS
JITDUMP("EH#%u has empty try, removing the try region and promoting the finally.\n", XTnum);
// There should be just one callfinally that invokes this
// finally, the one we found above. Verify this.
BasicBlock* firstCallFinallyRangeBlock = nullptr;
BasicBlock* endCallFinallyRangeBlock = nullptr;
bool verifiedSingleCallfinally = true;
ehGetCallFinallyBlockRange(XTnum, &firstCallFinallyRangeBlock, &endCallFinallyRangeBlock);
for (BasicBlock* block = firstCallFinallyRangeBlock; block != endCallFinallyRangeBlock; block = block->bbNext)
{
if ((block->bbJumpKind == BBJ_CALLFINALLY) && (block->bbJumpDest == firstHandlerBlock))
{
assert(block->isBBCallAlwaysPair());
if (block != callFinally)
{
JITDUMP("EH#%u found unexpected callfinally BB%02u; skipping.\n");
verifiedSingleCallfinally = false;
break;
}
block = block->bbNext;
}
}
if (!verifiedSingleCallfinally)
{
JITDUMP("EH#%u -- unexpectedly -- has multiple callfinallys; skipping.\n");
XTnum++;
assert(verifiedSingleCallfinally);
continue;
}
// Time to optimize.
//
// (1) Convert the callfinally to a normal jump to the handler
callFinally->bbJumpKind = BBJ_ALWAYS;
// Identify the leave block and the continuation
BasicBlock* const leave = callFinally->bbNext;
BasicBlock* const continuation = leave->bbJumpDest;
// (2) Cleanup the leave so it can be deleted by subsequent opts
assert((leave->bbFlags & BBF_KEEP_BBJ_ALWAYS) != 0);
leave->bbFlags &= ~BBF_KEEP_BBJ_ALWAYS;
// (3) Cleanup the continuation
fgCleanupContinuation(continuation);
// (4) Find enclosing try region for the try, if any, and
// update the try region for the blocks in the try. Note the
// handler region (if any) won't change.
//
// Kind of overkill to loop here, but hey.
for (BasicBlock* block = firstTryBlock; block != nullptr; block = block->bbNext)
{
// Look for blocks directly contained in this try, and
// update the try region appropriately.
//
// The try region for blocks transitively contained (say in a
// child try) will get updated by the subsequent call to
// fgRemoveEHTableEntry.
if (block->getTryIndex() == XTnum)
{
if (firstHandlerBlock->hasTryIndex())
{
block->setTryIndex(firstHandlerBlock->getTryIndex());
}
else
{
block->clearTryIndex();
}
}
if (block == firstTryBlock)
{
assert((block->bbFlags & BBF_TRY_BEG) != 0);
block->bbFlags &= ~BBF_TRY_BEG;
}
if (block == lastTryBlock)
{
break;
}
}
// (5) Update the directly contained handler blocks' handler index.
// Handler index of any nested blocks will update when we
// remove the EH table entry. Change handler exits to jump to
// the continuation. Clear catch type on handler entry.
for (BasicBlock* block = firstHandlerBlock; block != endHandlerBlock; block = block->bbNext)
{
if (block == firstHandlerBlock)
{
block->bbCatchTyp = BBCT_NONE;
}
if (block->getHndIndex() == XTnum)
{
if (firstTryBlock->hasHndIndex())
{
block->setHndIndex(firstTryBlock->getHndIndex());
}
else
{
block->clearHndIndex();
}
if (block->bbJumpKind == BBJ_EHFINALLYRET)
{
GenTreeStmt* finallyRet = block->lastStmt();
GenTreePtr finallyRetExpr = finallyRet->gtStmtExpr;
assert(finallyRetExpr->gtOper == GT_RETFILT);
fgRemoveStmt(block, finallyRet);
block->bbJumpKind = BBJ_ALWAYS;
block->bbJumpDest = continuation;
}
}
}
// (6) Remove the try-finally EH region. This will compact the
// EH table so XTnum now points at the next entry and will update
// the EH region indices of any nested EH in the (former) handler.
fgRemoveEHTableEntry(XTnum);
// Another one bites the dust...
emptyCount++;
}
if (emptyCount > 0)
{
JITDUMP("fgRemoveEmptyTry() optimized %u empty-try try-finally clauses\n", emptyCount);
#ifdef DEBUG
if (verbose)
{
printf("\n*************** After fgRemoveEmptyTry()\n");
fgDispBasicBlocks();
fgDispHandlerTab();
printf("\n");
}
fgVerifyHandlerTab();
fgDebugCheckBBlist(false, false);
#endif // DEBUG
}
}
//------------------------------------------------------------------------
// fgCloneFinally: Optimize normal exit path from a try/finally
//
@ -22776,7 +23058,7 @@ void Compiler::fgCloneFinally()
{
JITDUMP("\n*************** In fgCloneFinally()\n");
#if FEATURE_CORECLR
#ifdef FEATURE_CORECLR
bool enableCloning = true;
#else
// Finally cloning currently doesn't provide sufficient protection
@ -22784,7 +23066,7 @@ void Compiler::fgCloneFinally()
bool enableCloning = false;
#endif // FEATURE_CORECLR
#if DEBUG
#ifdef DEBUG
// Allow override to enable/disable.
enableCloning = (JitConfig.JitEnableFinallyCloning() == 1);
#endif // DEBUG
@ -23280,34 +23562,8 @@ void Compiler::fgCloneFinally()
BasicBlock* firstClonedBlock = blockMap[firstBlock];
firstClonedBlock->bbCatchTyp = BBCT_NONE;
// The normalCallFinallyReturn may be a finalStep block. It
// is now a normal block, since all the callfinallies that
// return to it are now going via the clone, so clear the
// special keep always flag.
normalCallFinallyReturn->bbFlags &= ~BBF_KEEP_BBJ_ALWAYS;
#if FEATURE_EH_FUNCLETS && defined(_TARGET_ARM_)
// Also, clear the finally target bit for arm
fgClearFinallyTargetBit(normalCallFinallyReturn);
#endif // FEATURE_EH_FUNCLETS && defined(_TARGET_ARM_)
#if !FEATURE_EH_FUNCLETS
// Remove the GT_END_LFIN from the post-try-finally block.
// remove it since there is no finally anymore. Note we only
// expect to see one statement.
bool foundEndLFin = false;
for (GenTreeStmt* stmt = normalCallFinallyReturn->firstStmt(); stmt != nullptr; stmt = stmt->gtNextStmt)
{
GenTreePtr expr = stmt->gtStmtExpr;
if (expr->gtOper == GT_END_LFIN)
{
assert(!foundEndLFin);
fgRemoveStmt(normalCallFinallyReturn, stmt);
foundEndLFin = true;
}
}
assert(foundEndLFin);
#endif
// Cleanup the contination
fgCleanupContinuation(normalCallFinallyReturn);
// Todo -- mark cloned blocks as a cloned finally....
@ -23530,3 +23786,44 @@ void Compiler::fgDebugCheckTryFinallyExits()
}
#endif // DEBUG
//------------------------------------------------------------------------
// fgCleanupContinuation: cleanup a finally continuation after a
// finally is removed or converted to normal control flow.
//
// Notes:
// The continuation is the block targeted by the second half of
// a callfinally/always pair.
//
// Used by finally cloning, empty try removal, and empty
// finally removal. Resets flags and removes finally artifacts.
void Compiler::fgCleanupContinuation(BasicBlock* continuation)
{
// The continuation may be a finalStep block.
// It is now a normal block, so clear the special keep
// always flag.
continuation->bbFlags &= ~BBF_KEEP_BBJ_ALWAYS;
#if FEATURE_EH_FUNCLETS && defined(_TARGET_ARM_)
// Also, clear the finally target bit for arm
fgClearFinallyTargetBit(continuation);
#endif // FEATURE_EH_FUNCLETS && defined(_TARGET_ARM_)
#if !FEATURE_EH_FUNCLETS
// Remove the GT_END_LFIN from the continuation,
// Note we only expect to see one such statement.
bool foundEndLFin = false;
for (GenTreeStmt* stmt = continuation->firstStmt(); stmt != nullptr; stmt = stmt->gtNextStmt)
{
GenTreePtr expr = stmt->gtStmtExpr;
if (expr->gtOper == GT_END_LFIN)
{
assert(!foundEndLFin);
fgRemoveStmt(continuation, stmt);
foundEndLFin = true;
}
}
assert(foundEndLFin);
#endif // !FEATURE_EH_FUNCLETS
}

View file

@ -277,8 +277,10 @@ CONFIG_INTEGER(JitEECallTimingInfo, W("JitEECallTimingInfo"), 0)
#if defined(DEBUG)
#if defined(FEATURE_CORECLR)
CONFIG_INTEGER(JitEnableFinallyCloning, W("JitEnableFinallyCloning"), 1)
CONFIG_INTEGER(JitEnableRemoveEmptyTry, W("JitEnableRemoveEmptyTry"), 1)
#else
CONFIG_INTEGER(JitEnableFinallyCloning, W("JitEnableFinallyCloning"), 0)
CONFIG_INTEGER(JitEnableRemoveEmptyTry, W("JitEnableRemoveEmptyTry"), 0)
#endif // defined(FEATURE_CORECLR)
#endif // DEBUG

View file

@ -16917,6 +16917,10 @@ void Compiler::fgMorph()
// RemoveEmptyFinally is disabled on ARM due to github issue #9013
#ifndef _TARGET_ARM_
fgRemoveEmptyTry();
EndPhase(PHASE_EMPTY_TRY);
fgRemoveEmptyFinally();
EndPhase(PHASE_EMPTY_FINALLY);