Skip to content

Commit

Permalink
Improve software exception handling performance (#108480)
Browse files Browse the repository at this point in the history
* Improve software exception handling performance

I have recently discovered that large part of the time spent in the EH
while handling software exception is in the RtlLookupFunctionEntry
Windows API. That API is called when unwinding native (non-managed)
frames on stack. The current implementation of the IL_Throw JIT helper
that is used to throw managed exceptions needs three unwinds to get to
the managed caller. Due to the two passes of EH, it means there are six
calls made to that API per throw.
This change reduces that to just a single unwind, which leads to about
12% improvement in single threaded scenarios and about 30% improvement
in multi threaded one for a case when an exception is thrown over 10
managed frames and then caught. It also leads to similar improvements in
async exception handling performance. On Linux, the multi-threaded gain is much higher,
I've measured 3 fold improvement.

An additional benefit of this change is removal of the helper method
frame from IL_Throw and IL_Rethrow, which contributes to the current
effort of getting rid of the helper method frames.

I have made this change for the new EH only.
  • Loading branch information
janvorli authored Oct 4, 2024
1 parent 2c5b609 commit 1fa1745
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 67 deletions.
3 changes: 3 additions & 0 deletions src/coreclr/inc/vptr_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ VPTR_CLASS(DebuggerSecurityCodeMarkFrame)
VPTR_CLASS(DebuggerExitFrame)
VPTR_CLASS(DebuggerU2MCatchHandlerFrame)
VPTR_CLASS(FaultingExceptionFrame)
#ifdef FEATURE_EH_FUNCLETS
VPTR_CLASS(SoftwareExceptionFrame)
#endif // FEATURE_EH_FUNCLETS
VPTR_CLASS(FuncEvalFrame)
VPTR_CLASS(HelperMethodFrame)
VPTR_CLASS(HelperMethodFrame_1OBJ)
Expand Down
76 changes: 76 additions & 0 deletions src/coreclr/vm/excep.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11553,3 +11553,79 @@ MethodDesc * GetUserMethodForILStub(Thread * pThread, UINT_PTR uStubSP, MethodDe
#endif // FEATURE_COMINTEROP
return pUserMD;
}


#ifdef FEATURE_EH_FUNCLETS

void SoftwareExceptionFrame::UpdateRegDisplay(const PREGDISPLAY pRD, bool updateFloats)
{
LIMITED_METHOD_DAC_CONTRACT;

#define CALLEE_SAVED_REGISTER(regname) pRD->pCurrentContext->regname = *dac_cast<PTR_SIZE_T>((TADDR)m_ContextPointers.regname);
ENUM_CALLEE_SAVED_REGISTERS();
#undef CALLEE_SAVED_REGISTER

#define CALLEE_SAVED_REGISTER(regname) pRD->pCurrentContextPointers->regname = m_ContextPointers.regname;
ENUM_CALLEE_SAVED_REGISTERS();
#undef CALLEE_SAVED_REGISTER

#define CALLEE_SAVED_REGISTER(regname) pRD->pCurrentContext->regname = m_Context.regname;
ENUM_FP_CALLEE_SAVED_REGISTERS();
#undef CALLEE_SAVED_REGISTER

SetIP(pRD->pCurrentContext, ::GetIP(&m_Context));
SetSP(pRD->pCurrentContext, ::GetSP(&m_Context));

pRD->ControlPC = ::GetIP(&m_Context);
pRD->SP = ::GetSP(&m_Context);

pRD->IsCallerContextValid = FALSE;
pRD->IsCallerSPValid = FALSE; // Don't add usage of this field. This is only temporary.
}

#ifndef DACCESS_COMPILE
//
// Init a new frame
//
void SoftwareExceptionFrame::Init()
{
WRAPPER_NO_CONTRACT;

#define CALLEE_SAVED_REGISTER(regname) m_ContextPointers.regname = NULL;
ENUM_CALLEE_SAVED_REGISTERS();
#undef CALLEE_SAVED_REGISTER

#ifndef TARGET_UNIX
Thread::VirtualUnwindCallFrame(&m_Context, &m_ContextPointers);
#else // !TARGET_UNIX
BOOL success = PAL_VirtualUnwind(&m_Context, &m_ContextPointers);
if (!success)
{
_ASSERTE(!"SoftwareExceptionFrame::Init failed");
EEPOLICY_HANDLE_FATAL_ERROR(COR_E_EXECUTIONENGINE);
}
#endif // !TARGET_UNIX

#define CALLEE_SAVED_REGISTER(regname) if (m_ContextPointers.regname == NULL) m_ContextPointers.regname = &m_Context.regname;
ENUM_CALLEE_SAVED_REGISTERS();
#undef CALLEE_SAVED_REGISTER

_ASSERTE(ExecutionManager::IsManagedCode(::GetIP(&m_Context)));

m_ReturnAddress = ::GetIP(&m_Context);
}

//
// Init and Link in a new frame
//
void SoftwareExceptionFrame::InitAndLink(Thread *pThread)
{
WRAPPER_NO_CONTRACT;

Init();

Push(pThread);
}

#endif // DACCESS_COMPILE
#endif // FEATURE_EH_FUNCLETS
67 changes: 67 additions & 0 deletions src/coreclr/vm/frames.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ FRAME_TYPE_NAME(ResumableFrame)
FRAME_TYPE_NAME(RedirectedThreadFrame)
#endif // FEATURE_HIJACK
FRAME_TYPE_NAME(FaultingExceptionFrame)
#ifdef FEATURE_EH_FUNCLETS
FRAME_TYPE_NAME(SoftwareExceptionFrame)
#endif // FEATURE_EH_FUNCLETS
#ifdef DEBUGGING_SUPPORTED
FRAME_TYPE_NAME(FuncEvalFrame)
#endif // DEBUGGING_SUPPORTED
Expand Down Expand Up @@ -1146,6 +1149,69 @@ class FaultingExceptionFrame : public Frame
DEFINE_VTABLE_GETTER_AND_DTOR(FaultingExceptionFrame)
};

#ifdef FEATURE_EH_FUNCLETS

class SoftwareExceptionFrame : public Frame
{
TADDR m_ReturnAddress;
T_CONTEXT m_Context;
T_KNONVOLATILE_CONTEXT_POINTERS m_ContextPointers;

VPTR_VTABLE_CLASS(SoftwareExceptionFrame, Frame)

public:
#ifndef DACCESS_COMPILE
SoftwareExceptionFrame() {
LIMITED_METHOD_CONTRACT;
}
#endif

virtual TADDR GetReturnAddressPtr()
{
LIMITED_METHOD_DAC_CONTRACT;
return PTR_HOST_MEMBER_TADDR(SoftwareExceptionFrame, this, m_ReturnAddress);
}

void Init();
void InitAndLink(Thread *pThread);

Interception GetInterception()
{
LIMITED_METHOD_DAC_CONTRACT;
return INTERCEPTION_EXCEPTION;
}

virtual ETransitionType GetTransitionType()
{
LIMITED_METHOD_DAC_CONTRACT;
return TT_InternalCall;
}

unsigned GetFrameAttribs()
{
LIMITED_METHOD_DAC_CONTRACT;
return FRAME_ATTR_EXCEPTION;
}

T_CONTEXT* GetContext()
{
LIMITED_METHOD_DAC_CONTRACT;
return &m_Context;
}

virtual BOOL NeedsUpdateRegDisplay()
{
return TRUE;
}

virtual void UpdateRegDisplay(const PREGDISPLAY, bool updateFloats = false);

// Keep as last entry in class
DEFINE_VTABLE_GETTER_AND_DTOR(SoftwareExceptionFrame)
};

#endif // FEATURE_EH_FUNCLETS

//-----------------------------------------------------------------------
// Frame for debugger function evaluation
//
Expand Down Expand Up @@ -3190,6 +3256,7 @@ class FrameWithCookie
void Poll() { WRAPPER_NO_CONTRACT; m_frame.Poll(); }
void SetStackPointerPtr(TADDR sp) { WRAPPER_NO_CONTRACT; m_frame.SetStackPointerPtr(sp); }
void InitAndLink(T_CONTEXT *pContext) { WRAPPER_NO_CONTRACT; m_frame.InitAndLink(pContext); }
void InitAndLink(Thread *pThread) { WRAPPER_NO_CONTRACT; m_frame.InitAndLink(pThread); }
void Init(Thread *pThread, OBJECTREF *pObjRefs, UINT numObjRefs, BOOL maybeInterior)
{ WRAPPER_NO_CONTRACT; m_frame.Init(pThread, pObjRefs, numObjRefs, maybeInterior); }
ValueClassInfo ** GetValueClassInfoList() { WRAPPER_NO_CONTRACT; return m_frame.GetValueClassInfoList(); }
Expand Down
136 changes: 69 additions & 67 deletions src/coreclr/vm/jithelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2675,65 +2675,68 @@ HCIMPLEND

/*************************************************************/

#ifdef FEATURE_EH_FUNCLETS
void ThrowNew(OBJECTREF oref)
HCIMPL1(void, IL_Throw, Object* obj)
{
if (oref == 0)
DispatchManagedException(kNullReferenceException);
else
if (!IsException(oref->GetMethodTable()))
FCALL_CONTRACT;

/* Make no assumptions about the current machine state */
ResetCurrentContext();

FC_GC_POLL_NOT_NEEDED(); // throws always open up for GC

OBJECTREF oref = ObjectToOBJECTREF(obj);

#ifdef FEATURE_EH_FUNCLETS
if (g_isNewExceptionHandlingEnabled)
{
GCPROTECT_BEGIN(oref);
Thread *pThread = GetThread();

WrapNonCompliantException(&oref);
FrameWithCookie<SoftwareExceptionFrame> exceptionFrame;
*(&exceptionFrame)->GetGSCookiePtr() = GetProcessGSCookie();
RtlCaptureContext(exceptionFrame.GetContext());
exceptionFrame.InitAndLink(pThread);

GCPROTECT_END();
}
else
{ // We know that the object derives from System.Exception
FC_CAN_TRIGGER_GC();

// If the flag indicating ForeignExceptionRaise has been set,
// then do not clear the "_stackTrace" field of the exception object.
if (GetThread()->GetExceptionState()->IsRaisingForeignException())
{
((EXCEPTIONREF)oref)->SetStackTraceString(NULL);
}
if (oref == 0)
DispatchManagedException(kNullReferenceException);
else
if (!IsException(oref->GetMethodTable()))
{
((EXCEPTIONREF)oref)->ClearStackTracePreservingRemoteStackTrace();
}
}
GCPROTECT_BEGIN(oref);

DispatchManagedException(oref);
}
#endif // FEATURE_EH_FUNCLETS
WrapNonCompliantException(&oref);

HCIMPL1(void, IL_Throw, Object* obj)
{
FCALL_CONTRACT;
GCPROTECT_END();
}
else
{ // We know that the object derives from System.Exception

/* Make no assumptions about the current machine state */
ResetCurrentContext();
// If the flag indicating ForeignExceptionRaise has been set,
// then do not clear the "_stackTrace" field of the exception object.
if (pThread->GetExceptionState()->IsRaisingForeignException())
{
((EXCEPTIONREF)oref)->SetStackTraceString(NULL);
}
else
{
((EXCEPTIONREF)oref)->ClearStackTracePreservingRemoteStackTrace();
}
}

FC_GC_POLL_NOT_NEEDED(); // throws always open up for GC
DispatchManagedException(oref, exceptionFrame.GetContext());
FC_CAN_TRIGGER_GC_END();
UNREACHABLE();
}
#endif // FEATURE_EH_FUNCLETS

HELPER_METHOD_FRAME_BEGIN_ATTRIB_NOPOLL(Frame::FRAME_ATTR_EXCEPTION); // Set up a frame

OBJECTREF oref = ObjectToOBJECTREF(obj);

#if defined(_DEBUG) && defined(TARGET_X86)
__helperframe.InsureInit(NULL);
g_ExceptionEIP = (LPVOID)__helperframe.GetReturnAddress();
#endif // defined(_DEBUG) && defined(TARGET_X86)

#ifdef FEATURE_EH_FUNCLETS
if (g_isNewExceptionHandlingEnabled)
{
ThrowNew(oref);
UNREACHABLE();
}
#endif

if (oref == 0)
COMPlusThrow(kNullReferenceException);
else
Expand Down Expand Up @@ -2768,49 +2771,48 @@ HCIMPLEND

/*************************************************************/

#ifdef FEATURE_EH_FUNCLETS
void RethrowNew()
HCIMPL0(void, IL_Rethrow)
{
Thread *pThread = GetThread();
FCALL_CONTRACT;

ExInfo *pActiveExInfo = (ExInfo*)pThread->GetExceptionState()->GetCurrentExceptionTracker();
FC_GC_POLL_NOT_NEEDED(); // throws always open up for GC

CONTEXT exceptionContext;
RtlCaptureContext(&exceptionContext);
#ifdef FEATURE_EH_FUNCLETS
if (g_isNewExceptionHandlingEnabled)
{
Thread *pThread = GetThread();

ExInfo exInfo(pThread, pActiveExInfo->m_ptrs.ExceptionRecord, &exceptionContext, ExKind::None);
FrameWithCookie<SoftwareExceptionFrame> exceptionFrame;
*(&exceptionFrame)->GetGSCookiePtr() = GetProcessGSCookie();
RtlCaptureContext(exceptionFrame.GetContext());
exceptionFrame.InitAndLink(pThread);

GCPROTECT_BEGIN(exInfo.m_exception);
PREPARE_NONVIRTUAL_CALLSITE(METHOD__EH__RH_RETHROW);
DECLARE_ARGHOLDER_ARRAY(args, 2);
ExInfo *pActiveExInfo = (ExInfo*)pThread->GetExceptionState()->GetCurrentExceptionTracker();

args[ARGNUM_0] = PTR_TO_ARGHOLDER(pActiveExInfo);
args[ARGNUM_1] = PTR_TO_ARGHOLDER(&exInfo);
ExInfo exInfo(pThread, pActiveExInfo->m_ptrs.ExceptionRecord, exceptionFrame.GetContext(), ExKind::None);

pThread->IncPreventAbort();
FC_CAN_TRIGGER_GC();

//Ex.RhRethrow(ref ExInfo activeExInfo, ref ExInfo exInfo)
CALL_MANAGED_METHOD_NORET(args)
GCPROTECT_END();
}
#endif // FEATURE_EH_FUNCLETS
GCPROTECT_BEGIN(exInfo.m_exception);
PREPARE_NONVIRTUAL_CALLSITE(METHOD__EH__RH_RETHROW);
DECLARE_ARGHOLDER_ARRAY(args, 2);

HCIMPL0(void, IL_Rethrow)
{
FCALL_CONTRACT;
args[ARGNUM_0] = PTR_TO_ARGHOLDER(pActiveExInfo);
args[ARGNUM_1] = PTR_TO_ARGHOLDER(&exInfo);

FC_GC_POLL_NOT_NEEDED(); // throws always open up for GC
pThread->IncPreventAbort();

HELPER_METHOD_FRAME_BEGIN_ATTRIB_NOPOLL(Frame::FRAME_ATTR_EXCEPTION); // Set up a frame
//Ex.RhRethrow(ref ExInfo activeExInfo, ref ExInfo exInfo)
CALL_MANAGED_METHOD_NORET(args)
GCPROTECT_END();

#ifdef FEATURE_EH_FUNCLETS
if (g_isNewExceptionHandlingEnabled)
{
RethrowNew();
FC_CAN_TRIGGER_GC_END();
UNREACHABLE();
}
#endif

HELPER_METHOD_FRAME_BEGIN_ATTRIB_NOPOLL(Frame::FRAME_ATTR_EXCEPTION); // Set up a frame

OBJECTREF throwable = GetThread()->GetThrowable();
if (throwable != NULL)
{
Expand Down

0 comments on commit 1fa1745

Please sign in to comment.