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

Commit

Permalink
Support Write-Thru of EH variables in LSRA
Browse files Browse the repository at this point in the history
Mark EH variables (those that are live in or out of exception regions) only as lvLiveInOutOfHndlr, not necessarily lvDoNotEnregister
During register allocation, mark these as write-thru, and mark all defs as write-thru, ensuring that the stack value is always valid.
Mark those defs with GTF_SPILLED (this the "reload" flag and is not currently used for pure defs) to indicate that it should be kept in the register.
Mark blocks that enter EH regions as having no predecessor, and set the location of all live-in vars to be on the stack.
Change genFnPrologCalleeRegArgs to store EH vars also to the stack if they have a register assignment.
Tune callee-save heuristics for EH vars.
Remove/modify mitigations
  • Loading branch information
CarolEidt committed Sep 13, 2019
1 parent cdc35c9 commit c3a8468
Show file tree
Hide file tree
Showing 13 changed files with 876 additions and 360 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,39 +25,32 @@ public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TS
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}

// enregistrer variables with 0 post-fix so they can be used in registers without EH forcing them to stack
// Capture references to Thread Contexts
Thread currentThread0 = Thread.CurrentThread;
Thread currentThread = currentThread0;
ExecutionContext? previousExecutionCtx0 = currentThread0._executionContext;
Thread currentThread = Thread.CurrentThread;

// Store current ExecutionContext and SynchronizationContext as "previousXxx".
// This allows us to restore them and undo any Context changes made in stateMachine.MoveNext
// so that they won't "leak" out of the first await.
ExecutionContext? previousExecutionCtx = previousExecutionCtx0;
SynchronizationContext? previousSyncCtx = currentThread0._synchronizationContext;
ExecutionContext? previousExecutionCtx = currentThread._executionContext;
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;

try
{
stateMachine.MoveNext();
}
finally
{
// Re-enregistrer variables post EH with 1 post-fix so they can be used in registers rather than from stack
SynchronizationContext? previousSyncCtx1 = previousSyncCtx;
Thread currentThread1 = currentThread;
// The common case is that these have not changed, so avoid the cost of a write barrier if not needed.
if (previousSyncCtx1 != currentThread1._synchronizationContext)
if (previousSyncCtx != currentThread._synchronizationContext)
{
// Restore changed SynchronizationContext back to previous
currentThread1._synchronizationContext = previousSyncCtx1;
currentThread._synchronizationContext = previousSyncCtx;
}

ExecutionContext? previousExecutionCtx1 = previousExecutionCtx;
ExecutionContext? currentExecutionCtx1 = currentThread1._executionContext;
if (previousExecutionCtx1 != currentExecutionCtx1)
ExecutionContext? currentExecutionCtx = currentThread._executionContext;
if (previousExecutionCtx != currentExecutionCtx)
{
ExecutionContext.RestoreChangedContextToThread(currentThread1, previousExecutionCtx1, currentExecutionCtx1);
ExecutionContext.RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentExecutionCtx);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,36 +134,34 @@ public static void Run(ExecutionContext executionContext, ContextCallback callba
internal static void RunInternal(ExecutionContext? executionContext, ContextCallback callback, object? state)
{
// Note: ExecutionContext.RunInternal is an extremely hot function and used by every await, ThreadPool execution, etc.
// Note: Manual enregistering may be addressed by "Exception Handling Write Through Optimization"
// https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/eh-writethru.md

// Enregister variables with 0 post-fix so they can be used in registers without EH forcing them to stack
// Capture references to Thread Contexts
Thread currentThread0 = Thread.CurrentThread;
Thread currentThread = currentThread0;
ExecutionContext? previousExecutionCtx0 = currentThread0._executionContext;
Thread currentThread = Thread.CurrentThread;
// previousExecutionCtx is live across an EH boundary. It is conditionally defined, thus has two
// definition points. In order to avoid forcing both of those to the stack, we define a temporary
// variable and then store that to the value that lives across the EH boundary.
ExecutionContext? previousExecutionCtx0 = currentThread._executionContext;
if (previousExecutionCtx0 != null && previousExecutionCtx0.m_isDefault)
{
// Default is a null ExecutionContext internally
previousExecutionCtx0 = null;
}
ExecutionContext? previousExecutionCtx = previousExecutionCtx0;

// Store current ExecutionContext and SynchronizationContext as "previousXxx".
// This allows us to restore them and undo any Context changes made in callback.Invoke
// so that they won't "leak" back into caller.
// These variables will cross EH so be forced to stack
ExecutionContext? previousExecutionCtx = previousExecutionCtx0;
SynchronizationContext? previousSyncCtx = currentThread0._synchronizationContext;
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;

if (executionContext != null && executionContext.m_isDefault)
{
// Default is a null ExecutionContext internally
executionContext = null;
}

if (previousExecutionCtx0 != executionContext)
if (previousExecutionCtx != executionContext)
{
RestoreChangedContextToThread(currentThread0, executionContext, previousExecutionCtx0);
RestoreChangedContextToThread(currentThread, executionContext, previousExecutionCtx);
}

ExceptionDispatchInfo? edi = null;
Expand All @@ -179,21 +177,17 @@ internal static void RunInternal(ExecutionContext? executionContext, ContextCall
edi = ExceptionDispatchInfo.Capture(ex);
}

// Re-enregistrer variables post EH with 1 post-fix so they can be used in registers rather than from stack
SynchronizationContext? previousSyncCtx1 = previousSyncCtx;
Thread currentThread1 = currentThread;
// The common case is that these have not changed, so avoid the cost of a write barrier if not needed.
if (currentThread1._synchronizationContext != previousSyncCtx1)
if (currentThread._synchronizationContext != previousSyncCtx)
{
// Restore changed SynchronizationContext back to previous
currentThread1._synchronizationContext = previousSyncCtx1;
currentThread._synchronizationContext = previousSyncCtx;
}

ExecutionContext? previousExecutionCtx1 = previousExecutionCtx;
ExecutionContext? currentExecutionCtx1 = currentThread1._executionContext;
if (currentExecutionCtx1 != previousExecutionCtx1)
ExecutionContext? currentExecutionCtx = currentThread._executionContext;
if (currentExecutionCtx != previousExecutionCtx)
{
RestoreChangedContextToThread(currentThread1, previousExecutionCtx1, currentExecutionCtx1);
RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentExecutionCtx);
}

// If exception was thrown by callback, rethrow it now original contexts are restored
Expand All @@ -204,36 +198,35 @@ internal static void RunInternal(ExecutionContext? executionContext, ContextCall
internal static void RunInternal<TState>(ExecutionContext? executionContext, ContextCallback<TState> callback, ref TState state)
{
// Note: ExecutionContext.RunInternal is an extremely hot function and used by every await, ThreadPool execution, etc.
// Note: Manual enregistering may be addressed by "Exception Handling Write Through Optimization"
// https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/eh-writethru.md

// Enregister variables with 0 post-fix so they can be used in registers without EH forcing them to stack
// Capture references to Thread Contexts
Thread currentThread0 = Thread.CurrentThread;
Thread currentThread = currentThread0;
ExecutionContext? previousExecutionCtx0 = currentThread0._executionContext;
Thread currentThread = Thread.CurrentThread;
// previousExecutionCtx is live across an EH boundary. It is conditionally defined, thus has two
// definition points. In order to avoid forcing both of those to the stack, we define a temporary
// variable and then store that to the value that lives across the EH boundary.
ExecutionContext? previousExecutionCtx0 = currentThread._executionContext;
if (previousExecutionCtx0 != null && previousExecutionCtx0.m_isDefault)
{
// Default is a null ExecutionContext internally
previousExecutionCtx0 = null;
}
ExecutionContext? previousExecutionCtx = previousExecutionCtx0;

// Store current ExecutionContext and SynchronizationContext as "previousXxx".
// This allows us to restore them and undo any Context changes made in callback.Invoke
// so that they won't "leak" back into caller.
// These variables will cross EH so be forced to stack
ExecutionContext? previousExecutionCtx = previousExecutionCtx0;
SynchronizationContext? previousSyncCtx = currentThread0._synchronizationContext;
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;

if (executionContext != null && executionContext.m_isDefault)
{
// Default is a null ExecutionContext internally
executionContext = null;
}

if (previousExecutionCtx0 != executionContext)
if (previousExecutionCtx != executionContext)
{
RestoreChangedContextToThread(currentThread0, executionContext, previousExecutionCtx0);
RestoreChangedContextToThread(currentThread, executionContext, previousExecutionCtx);
}

ExceptionDispatchInfo? edi = null;
Expand All @@ -249,21 +242,17 @@ internal static void RunInternal<TState>(ExecutionContext? executionContext, Con
edi = ExceptionDispatchInfo.Capture(ex);
}

// Re-enregistrer variables post EH with 1 post-fix so they can be used in registers rather than from stack
SynchronizationContext? previousSyncCtx1 = previousSyncCtx;
Thread currentThread1 = currentThread;
// The common case is that these have not changed, so avoid the cost of a write barrier if not needed.
if (currentThread1._synchronizationContext != previousSyncCtx1)
if (currentThread._synchronizationContext != previousSyncCtx)
{
// Restore changed SynchronizationContext back to previous
currentThread1._synchronizationContext = previousSyncCtx1;
currentThread._synchronizationContext = previousSyncCtx;
}

ExecutionContext? previousExecutionCtx1 = previousExecutionCtx;
ExecutionContext? currentExecutionCtx1 = currentThread1._executionContext;
if (currentExecutionCtx1 != previousExecutionCtx1)
ExecutionContext? currentExecutionCtx = currentThread._executionContext;
if (currentExecutionCtx != previousExecutionCtx)
{
RestoreChangedContextToThread(currentThread1, previousExecutionCtx1, currentExecutionCtx1);
RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentExecutionCtx);
}

// If exception was thrown by callback, rethrow it now original contexts are restored
Expand Down
68 changes: 41 additions & 27 deletions src/jit/codegencommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,10 @@ void CodeGenInterface::genUpdateRegLife(const LclVarDsc* varDsc, bool isBorn, bo
}
else
{
assert((regSet.rsMaskVars & regMask) == 0);
// If this is going live, the register must not have a variable in it, except
// in the case of an exception variable, which may be already treated as live
// in the register.
assert(varDsc->lvLiveInOutOfHndlr || ((regSet.rsMaskVars & regMask) == 0));
regSet.AddMaskVars(regMask);
}
}
Expand Down Expand Up @@ -666,12 +669,14 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife)
unsigned deadVarIndex = 0;
while (deadIter.NextElem(&deadVarIndex))
{
unsigned varNum = lvaTrackedIndexToLclNum(deadVarIndex);
LclVarDsc* varDsc = lvaGetDesc(varNum);
bool isGCRef = (varDsc->TypeGet() == TYP_REF);
bool isByRef = (varDsc->TypeGet() == TYP_BYREF);
unsigned varNum = lvaTrackedIndexToLclNum(deadVarIndex);
LclVarDsc* varDsc = lvaGetDesc(varNum);
bool isGCRef = (varDsc->TypeGet() == TYP_REF);
bool isByRef = (varDsc->TypeGet() == TYP_BYREF);
bool isInReg = varDsc->lvIsInReg();
bool isInMemory = !isInReg || varDsc->lvLiveInOutOfHndlr;

if (varDsc->lvIsInReg())
if (isInReg)
{
// TODO-Cleanup: Move the code from compUpdateLifeVar to genUpdateRegLife that updates the
// gc sets
Expand All @@ -686,8 +691,8 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife)
}
codeGen->genUpdateRegLife(varDsc, false /*isBorn*/, true /*isDying*/ DEBUGARG(nullptr));
}
// This isn't in a register, so update the gcVarPtrSetCur.
else if (isGCRef || isByRef)
// Update the gcVarPtrSetCur if it is in memory.
if (isInMemory && (isGCRef || isByRef))
{
VarSetOps::RemoveElemD(this, codeGen->gcInfo.gcVarPtrSetCur, deadVarIndex);
JITDUMP("\t\t\t\t\t\t\tV%02u becoming dead\n", varNum);
Expand All @@ -709,13 +714,16 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife)

if (varDsc->lvIsInReg())
{
#ifdef DEBUG
if (VarSetOps::IsMember(this, codeGen->gcInfo.gcVarPtrSetCur, bornVarIndex))
if (!varDsc->lvLiveInOutOfHndlr)
{
JITDUMP("\t\t\t\t\t\t\tRemoving V%02u from gcVarPtrSetCur\n", varNum);
}
#ifdef DEBUG
if (VarSetOps::IsMember(this, codeGen->gcInfo.gcVarPtrSetCur, bornVarIndex))
{
JITDUMP("\t\t\t\t\t\t\tRemoving V%02u from gcVarPtrSetCur\n", varNum);
}
#endif // DEBUG
VarSetOps::RemoveElemD(this, codeGen->gcInfo.gcVarPtrSetCur, bornVarIndex);
VarSetOps::RemoveElemD(this, codeGen->gcInfo.gcVarPtrSetCur, bornVarIndex);
}
codeGen->genUpdateRegLife(varDsc, true /*isBorn*/, false /*isDying*/ DEBUGARG(nullptr));
regMaskTP regMask = varDsc->lvRegMask();
if (isGCRef)
Expand Down Expand Up @@ -3190,6 +3198,7 @@ void CodeGen::genFnPrologCalleeRegArgs(regNumber xtraReg, bool* pXtraRegClobbere
// 1 means the first part of a register argument
// 2, 3 or 4 means the second,third or fourth part of a multireg argument
bool stackArg; // true if the argument gets homed to the stack
bool writeThru; // true if the argument gets homed to both stack and register
bool processed; // true after we've processed the argument (and it is in its final location)
bool circular; // true if this register participates in a circular dependency loop.

Expand Down Expand Up @@ -3526,6 +3535,7 @@ void CodeGen::genFnPrologCalleeRegArgs(regNumber xtraReg, bool* pXtraRegClobbere
}

regArgTab[regArgNum + i].processed = false;
regArgTab[regArgNum + i].writeThru = (varDsc->lvIsInReg() && varDsc->lvLiveInOutOfHndlr);

/* mark stack arguments since we will take care of those first */
regArgTab[regArgNum + i].stackArg = (varDsc->lvIsInReg()) ? false : true;
Expand Down Expand Up @@ -3686,9 +3696,9 @@ void CodeGen::genFnPrologCalleeRegArgs(regNumber xtraReg, bool* pXtraRegClobbere
noway_assert(((regArgMaskLive & RBM_FLTARG_REGS) == 0) &&
"Homing of float argument registers with circular dependencies not implemented.");

/* Now move the arguments to their locations.
* First consider ones that go on the stack since they may
* free some registers. */
// Now move the arguments to their locations.
// First consider ones that go on the stack since they may free some registers.
// Also home writeThru args, since they're also homed to the stack.

regArgMaskLive = regState->rsCalleeRegArgMaskLiveIn; // reset the live in to what it was at the start
for (argNum = 0; argNum < argMax; argNum++)
Expand Down Expand Up @@ -3726,7 +3736,7 @@ void CodeGen::genFnPrologCalleeRegArgs(regNumber xtraReg, bool* pXtraRegClobbere
// If not a stack arg go to the next one
if (varDsc->lvType == TYP_LONG)
{
if (regArgTab[argNum].slot == 1 && !regArgTab[argNum].stackArg)
if (regArgTab[argNum].slot == 1 && !regArgTab[argNum].stackArg && !regArgTab[argNum].writeThru)
{
continue;
}
Expand All @@ -3739,7 +3749,7 @@ void CodeGen::genFnPrologCalleeRegArgs(regNumber xtraReg, bool* pXtraRegClobbere
#endif // !_TARGET_64BIT_
{
// If not a stack arg go to the next one
if (!regArgTab[argNum].stackArg)
if (!regArgTab[argNum].stackArg && !regArgTab[argNum].writeThru)
{
continue;
}
Expand All @@ -3760,7 +3770,7 @@ void CodeGen::genFnPrologCalleeRegArgs(regNumber xtraReg, bool* pXtraRegClobbere

noway_assert(varDsc->lvIsParam);
noway_assert(varDsc->lvIsRegArg);
noway_assert(varDsc->lvIsInReg() == false ||
noway_assert(varDsc->lvIsInReg() == false || varDsc->lvLiveInOutOfHndlr ||
(varDsc->lvType == TYP_LONG && varDsc->lvOtherReg == REG_STK && regArgTab[argNum].slot == 2));

var_types storeType = TYP_UNDEF;
Expand Down Expand Up @@ -3827,13 +3837,15 @@ void CodeGen::genFnPrologCalleeRegArgs(regNumber xtraReg, bool* pXtraRegClobbere
#endif // USING_SCOPE_INFO
}

/* mark the argument as processed */

regArgTab[argNum].processed = true;
regArgMaskLive &= ~genRegMask(srcRegNum);
// Mark the argument as processed.
if (!regArgTab[argNum].writeThru)
{
regArgTab[argNum].processed = true;
regArgMaskLive &= ~genRegMask(srcRegNum);
}

#if defined(_TARGET_ARM_)
if (storeType == TYP_DOUBLE)
if ((storeType == TYP_DOUBLE) && !regArgTab[argNum].writeThru)
{
regArgTab[argNum + 1].processed = true;
regArgMaskLive &= ~genRegMask(REG_NEXT(srcRegNum));
Expand Down Expand Up @@ -4529,7 +4541,7 @@ void CodeGen::genCheckUseBlockInit()
{
if (!varDsc->lvRegister)
{
if (!varDsc->lvIsInReg())
if (!varDsc->lvIsInReg() || varDsc->lvLiveInOutOfHndlr)
{
// Var is on the stack at entry.
initStkLclCnt +=
Expand Down Expand Up @@ -7728,7 +7740,9 @@ void CodeGen::genFnProlog()
continue;
}

if (varDsc->lvIsInReg())
bool isInReg = varDsc->lvIsInReg();
bool isInMemory = !isInReg || varDsc->lvLiveInOutOfHndlr;
if (isInReg)
{
regMaskTP regMask = genRegMask(varDsc->lvRegNum);
if (!varDsc->IsFloatRegType())
Expand Down Expand Up @@ -7759,7 +7773,7 @@ void CodeGen::genFnProlog()
initFltRegs |= regMask;
}
}
else
if (isInMemory)
{
INIT_STK:

Expand Down
Loading

0 comments on commit c3a8468

Please sign in to comment.