Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model - #114582

Merged
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts
Apr 22, 2025
Merged

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model#114582
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts

Conversation

@davidwrighton

@davidwrightondavidwrighton commented Apr 11, 2025

Copy link
Copy Markdown
Member
  • Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
    • This allows skipping setting the InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
    • On X86 this also allows skipping using an EH prolog for this function
    • Only Update the MethodRegionInfo if there are EH regions to walk
  • Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
    • Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
    • Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checking it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
    • Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
    • For reflection emitted modules, always consider the status of this flag to be computed, but if we emit the controlling attribute, adjust the current cached computed value. NOTE: this is a minor breaking change from the previous behavior.
  • Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
  • Improve the performance of StackTraceInfo::AppendElement
    • It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
    • Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
    • Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
    • Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
  • Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
  • For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
  • Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work for better inlining behavior.

This PR improves the performance of deep stack EH throwing by micro-optimizing a number of scenarios. In particular in the Windows X86 Funclet model, it achieves about a 15% improvement to a simple benchmark.

…rticularly in the X86 with Funclets model
- Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
- This allows skipping setting hte InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
- On X86 this also allows skipping using an EH prolog for this function
- Only Update the MethodRegionInfo if there are EH regions to walk
- Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
- Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
- Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checkinging it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
- Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
- Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
- Improve the performance of StackTraceInfo::AppendElement
- It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
- Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
- Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
- Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
- Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
- For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
- Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work.
CopilotAI review requested due to automatic review settings April 11, 2025 23:24
@ghostghost added the area-ExceptionHandling-coreclr only use for closed issues label Apr 11, 2025

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.


class StackTraceInfo
{
struct StackTraceArrayProtect

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding inline documentation to clarify the roles of m_pStackTraceArray and m_pStackTraceArrayNew within StackTraceArrayProtect to improve future maintainability.

Copilot uses AI. Check for mistakes.
}
CONTRACTL_END

_ASSERTE(IsRuntimeWrapExceptionsStatusComputed());

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

It may be helpful to document the assumption that the runtime wrap exception status is computed before calling IsRuntimeWrapExceptionsDuringEH, to aid future maintainers in understanding the precondition.

Copilot uses AI. Check for mistakes.
GCPROTECT_BEGIN(newStackTrace);

size_t stackTraceCapacity = pStackTrace->Capacity();
size_t stackTraceCapacity = pStackTraceProtected->m_pStackTraceArray.Capacity();

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding a comment to explain the use of m_pStackTraceArray within the StackTraceArrayProtect structure, to clarify how capacity and copying are managed in the new implementation.

Copilot uses AI. Check for mistakes.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if !defined(DACCESS_COMPILE)

#ifdef DEBUGGING_SUPPORTED
static DebuggerExState m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
static DebuggerExState m_emptyDebuggerExState;
static DebuggerExState s_emptyDebuggerExState;

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if defined(_MSC_VER)
#pragma warning(default : 4640)
#endif
return &m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return &m_emptyDebuggerExState;
return &s_emptyDebuggerExState;

_ASSERTE(hdrInfoSize != 0);
m_hdrInfoTable = (PTR_CBYTE)gcInfoToken.Info + hdrInfoSize;
}
GCInfoToken gcInfoToken = GetGCInfoToken();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
GCInfoToken gcInfoToken = GetGCInfoToken();
_ASSERTE(m_hdrInfoTable == NULL);
GCInfoToken gcInfoToken = GetGCInfoToken();

@filipnavara

filipnavara commented Apr 13, 2025

Copy link
Copy Markdown
Member

Some of the test failures look legit:

> C:\h\w\A8C60910\w\A155092C\e\baseservices\baseservices\../callconvs/TestCallingConventions/TestCallingConventions.cmd
Assert failure(PID 3332 [0x00000d04], Thread: 1432 [0x0598]): pExInfo->m_pMDToReportFunctionLeave != NULL
CORECLR! NotifyExceptionPassStarted + 0x190 (0x00007fff`a273a730)
CORECLR! SfiInit + 0x128 (0x00007fff`a273d158)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c416d4)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c40cc8)
CORECLR! CallDescrWorkerInternal + 0x84 (0x00007fff`a2441f94)
CORECLR! CallDescrWorkerWithHandler + 0x120 (0x00007fff`a25bc5e8)
CORECLR! DispatchCallSimple + 0xF8 (0x00007fff`a25bce28)
CORECLR! DispatchManagedException + 0x238 (0x00007fff`a27387f8)
CORECLR! IL_Throw + 0x160 (0x00007fff`a2637b30)
<no module>! <no symbol> + 0x0 (0x00007fff`42f74094)
File: D:\a\_work\1\s\src\coreclr\vm\exceptionhandling.cpp:3680
Image: C:\h\w\A8C60910\p\corerun.exe

I didn't try to track it down yet, but it seems related to the NotifyFunctionEnter change.

Comment threadsrc/coreclr/vm/clrex.h Outdated
@filipnavara

Copy link
Copy Markdown
Member

Seems like the tests passed. Can we get this merged or is there still outstanding work to be done?

@davidwrighton
davidwrighton merged commit 2224669 into dotnet:mainApr 22, 2025
@filipnavara

Copy link
Copy Markdown
Member

Thanks a lot!

@davidwrighton

Copy link
Copy Markdown
MemberAuthor

@filipnavara I'm sorry about the delay, I've been unavoidably away from work for the last week, and I'm only now getting back. I didn't get a chance to look into the debugger scenarios, so I'll try to see if I can get that done some time this week, but I may not be able to.

@filipnavara

Copy link
Copy Markdown
Member

No worries. I appreciate any help with this! Thanks for keeping me in the loop.

mikem8361 pushed a commit to mikem8361/runtime that referenced this pull request Apr 24, 2025
This was caused by changes in PR dotnet#114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
mikem8361 pushed a commit that referenced this pull request Apr 25, 2025
)
This was caused by changes in PR #114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-ExceptionHandling-coreclronly use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@davidwrighton@filipnavara@jkotas@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model - #114582

Merged
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts
Apr 22, 2025
Merged

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model#114582
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts

Conversation

@davidwrighton

@davidwrightondavidwrighton commented Apr 11, 2025

Copy link
Copy Markdown
Member
  • Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
    • This allows skipping setting the InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
    • On X86 this also allows skipping using an EH prolog for this function
    • Only Update the MethodRegionInfo if there are EH regions to walk
  • Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
    • Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
    • Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checking it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
    • Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
    • For reflection emitted modules, always consider the status of this flag to be computed, but if we emit the controlling attribute, adjust the current cached computed value. NOTE: this is a minor breaking change from the previous behavior.
  • Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
  • Improve the performance of StackTraceInfo::AppendElement
    • It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
    • Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
    • Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
    • Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
  • Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
  • For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
  • Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work for better inlining behavior.

This PR improves the performance of deep stack EH throwing by micro-optimizing a number of scenarios. In particular in the Windows X86 Funclet model, it achieves about a 15% improvement to a simple benchmark.

…rticularly in the X86 with Funclets model
- Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
- This allows skipping setting hte InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
- On X86 this also allows skipping using an EH prolog for this function
- Only Update the MethodRegionInfo if there are EH regions to walk
- Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
- Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
- Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checkinging it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
- Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
- Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
- Improve the performance of StackTraceInfo::AppendElement
- It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
- Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
- Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
- Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
- Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
- For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
- Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work.
CopilotAI review requested due to automatic review settings April 11, 2025 23:24
@ghostghost added the area-ExceptionHandling-coreclr only use for closed issues label Apr 11, 2025

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.


class StackTraceInfo
{
struct StackTraceArrayProtect

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding inline documentation to clarify the roles of m_pStackTraceArray and m_pStackTraceArrayNew within StackTraceArrayProtect to improve future maintainability.

Copilot uses AI. Check for mistakes.
}
CONTRACTL_END

_ASSERTE(IsRuntimeWrapExceptionsStatusComputed());

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

It may be helpful to document the assumption that the runtime wrap exception status is computed before calling IsRuntimeWrapExceptionsDuringEH, to aid future maintainers in understanding the precondition.

Copilot uses AI. Check for mistakes.
GCPROTECT_BEGIN(newStackTrace);

size_t stackTraceCapacity = pStackTrace->Capacity();
size_t stackTraceCapacity = pStackTraceProtected->m_pStackTraceArray.Capacity();

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding a comment to explain the use of m_pStackTraceArray within the StackTraceArrayProtect structure, to clarify how capacity and copying are managed in the new implementation.

Copilot uses AI. Check for mistakes.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if !defined(DACCESS_COMPILE)

#ifdef DEBUGGING_SUPPORTED
static DebuggerExState m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
static DebuggerExState m_emptyDebuggerExState;
static DebuggerExState s_emptyDebuggerExState;

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if defined(_MSC_VER)
#pragma warning(default : 4640)
#endif
return &m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return &m_emptyDebuggerExState;
return &s_emptyDebuggerExState;

_ASSERTE(hdrInfoSize != 0);
m_hdrInfoTable = (PTR_CBYTE)gcInfoToken.Info + hdrInfoSize;
}
GCInfoToken gcInfoToken = GetGCInfoToken();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
GCInfoToken gcInfoToken = GetGCInfoToken();
_ASSERTE(m_hdrInfoTable == NULL);
GCInfoToken gcInfoToken = GetGCInfoToken();

@filipnavara

filipnavara commented Apr 13, 2025

Copy link
Copy Markdown
Member

Some of the test failures look legit:

> C:\h\w\A8C60910\w\A155092C\e\baseservices\baseservices\../callconvs/TestCallingConventions/TestCallingConventions.cmd
Assert failure(PID 3332 [0x00000d04], Thread: 1432 [0x0598]): pExInfo->m_pMDToReportFunctionLeave != NULL
CORECLR! NotifyExceptionPassStarted + 0x190 (0x00007fff`a273a730)
CORECLR! SfiInit + 0x128 (0x00007fff`a273d158)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c416d4)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c40cc8)
CORECLR! CallDescrWorkerInternal + 0x84 (0x00007fff`a2441f94)
CORECLR! CallDescrWorkerWithHandler + 0x120 (0x00007fff`a25bc5e8)
CORECLR! DispatchCallSimple + 0xF8 (0x00007fff`a25bce28)
CORECLR! DispatchManagedException + 0x238 (0x00007fff`a27387f8)
CORECLR! IL_Throw + 0x160 (0x00007fff`a2637b30)
<no module>! <no symbol> + 0x0 (0x00007fff`42f74094)
File: D:\a\_work\1\s\src\coreclr\vm\exceptionhandling.cpp:3680
Image: C:\h\w\A8C60910\p\corerun.exe

I didn't try to track it down yet, but it seems related to the NotifyFunctionEnter change.

Comment threadsrc/coreclr/vm/clrex.h Outdated
@filipnavara

Copy link
Copy Markdown
Member

Seems like the tests passed. Can we get this merged or is there still outstanding work to be done?

@davidwrighton
davidwrighton merged commit 2224669 into dotnet:mainApr 22, 2025
@filipnavara

Copy link
Copy Markdown
Member

Thanks a lot!

@davidwrighton

Copy link
Copy Markdown
MemberAuthor

@filipnavara I'm sorry about the delay, I've been unavoidably away from work for the last week, and I'm only now getting back. I didn't get a chance to look into the debugger scenarios, so I'll try to see if I can get that done some time this week, but I may not be able to.

@filipnavara

Copy link
Copy Markdown
Member

No worries. I appreciate any help with this! Thanks for keeping me in the loop.

mikem8361 pushed a commit to mikem8361/runtime that referenced this pull request Apr 24, 2025
This was caused by changes in PR dotnet#114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
mikem8361 pushed a commit that referenced this pull request Apr 25, 2025
)
This was caused by changes in PR #114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-ExceptionHandling-coreclronly use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@davidwrighton@filipnavara@jkotas@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model - #114582

Merged
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts
Apr 22, 2025
Merged

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model#114582
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts

Conversation

@davidwrighton

@davidwrightondavidwrighton commented Apr 11, 2025

Copy link
Copy Markdown
Member
  • Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
    • This allows skipping setting the InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
    • On X86 this also allows skipping using an EH prolog for this function
    • Only Update the MethodRegionInfo if there are EH regions to walk
  • Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
    • Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
    • Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checking it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
    • Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
    • For reflection emitted modules, always consider the status of this flag to be computed, but if we emit the controlling attribute, adjust the current cached computed value. NOTE: this is a minor breaking change from the previous behavior.
  • Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
  • Improve the performance of StackTraceInfo::AppendElement
    • It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
    • Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
    • Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
    • Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
  • Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
  • For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
  • Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work for better inlining behavior.

This PR improves the performance of deep stack EH throwing by micro-optimizing a number of scenarios. In particular in the Windows X86 Funclet model, it achieves about a 15% improvement to a simple benchmark.

…rticularly in the X86 with Funclets model
- Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
- This allows skipping setting hte InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
- On X86 this also allows skipping using an EH prolog for this function
- Only Update the MethodRegionInfo if there are EH regions to walk
- Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
- Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
- Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checkinging it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
- Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
- Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
- Improve the performance of StackTraceInfo::AppendElement
- It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
- Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
- Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
- Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
- Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
- For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
- Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work.
CopilotAI review requested due to automatic review settings April 11, 2025 23:24
@ghostghost added the area-ExceptionHandling-coreclr only use for closed issues label Apr 11, 2025

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.


class StackTraceInfo
{
struct StackTraceArrayProtect

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding inline documentation to clarify the roles of m_pStackTraceArray and m_pStackTraceArrayNew within StackTraceArrayProtect to improve future maintainability.

Copilot uses AI. Check for mistakes.
}
CONTRACTL_END

_ASSERTE(IsRuntimeWrapExceptionsStatusComputed());

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

It may be helpful to document the assumption that the runtime wrap exception status is computed before calling IsRuntimeWrapExceptionsDuringEH, to aid future maintainers in understanding the precondition.

Copilot uses AI. Check for mistakes.
GCPROTECT_BEGIN(newStackTrace);

size_t stackTraceCapacity = pStackTrace->Capacity();
size_t stackTraceCapacity = pStackTraceProtected->m_pStackTraceArray.Capacity();

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding a comment to explain the use of m_pStackTraceArray within the StackTraceArrayProtect structure, to clarify how capacity and copying are managed in the new implementation.

Copilot uses AI. Check for mistakes.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if !defined(DACCESS_COMPILE)

#ifdef DEBUGGING_SUPPORTED
static DebuggerExState m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
static DebuggerExState m_emptyDebuggerExState;
static DebuggerExState s_emptyDebuggerExState;

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if defined(_MSC_VER)
#pragma warning(default : 4640)
#endif
return &m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return &m_emptyDebuggerExState;
return &s_emptyDebuggerExState;

_ASSERTE(hdrInfoSize != 0);
m_hdrInfoTable = (PTR_CBYTE)gcInfoToken.Info + hdrInfoSize;
}
GCInfoToken gcInfoToken = GetGCInfoToken();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
GCInfoToken gcInfoToken = GetGCInfoToken();
_ASSERTE(m_hdrInfoTable == NULL);
GCInfoToken gcInfoToken = GetGCInfoToken();

@filipnavara

filipnavara commented Apr 13, 2025

Copy link
Copy Markdown
Member

Some of the test failures look legit:

> C:\h\w\A8C60910\w\A155092C\e\baseservices\baseservices\../callconvs/TestCallingConventions/TestCallingConventions.cmd
Assert failure(PID 3332 [0x00000d04], Thread: 1432 [0x0598]): pExInfo->m_pMDToReportFunctionLeave != NULL
CORECLR! NotifyExceptionPassStarted + 0x190 (0x00007fff`a273a730)
CORECLR! SfiInit + 0x128 (0x00007fff`a273d158)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c416d4)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c40cc8)
CORECLR! CallDescrWorkerInternal + 0x84 (0x00007fff`a2441f94)
CORECLR! CallDescrWorkerWithHandler + 0x120 (0x00007fff`a25bc5e8)
CORECLR! DispatchCallSimple + 0xF8 (0x00007fff`a25bce28)
CORECLR! DispatchManagedException + 0x238 (0x00007fff`a27387f8)
CORECLR! IL_Throw + 0x160 (0x00007fff`a2637b30)
<no module>! <no symbol> + 0x0 (0x00007fff`42f74094)
File: D:\a\_work\1\s\src\coreclr\vm\exceptionhandling.cpp:3680
Image: C:\h\w\A8C60910\p\corerun.exe

I didn't try to track it down yet, but it seems related to the NotifyFunctionEnter change.

Comment threadsrc/coreclr/vm/clrex.h Outdated
@filipnavara

Copy link
Copy Markdown
Member

Seems like the tests passed. Can we get this merged or is there still outstanding work to be done?

@davidwrighton
davidwrighton merged commit 2224669 into dotnet:mainApr 22, 2025
@filipnavara

Copy link
Copy Markdown
Member

Thanks a lot!

@davidwrighton

Copy link
Copy Markdown
MemberAuthor

@filipnavara I'm sorry about the delay, I've been unavoidably away from work for the last week, and I'm only now getting back. I didn't get a chance to look into the debugger scenarios, so I'll try to see if I can get that done some time this week, but I may not be able to.

@filipnavara

Copy link
Copy Markdown
Member

No worries. I appreciate any help with this! Thanks for keeping me in the loop.

mikem8361 pushed a commit to mikem8361/runtime that referenced this pull request Apr 24, 2025
This was caused by changes in PR dotnet#114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
mikem8361 pushed a commit that referenced this pull request Apr 25, 2025
)
This was caused by changes in PR #114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-ExceptionHandling-coreclronly use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@davidwrighton@filipnavara@jkotas@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model - #114582

Merged
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts
Apr 22, 2025
Merged

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model#114582
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts

Conversation

@davidwrighton

@davidwrightondavidwrighton commented Apr 11, 2025

Copy link
Copy Markdown
Member
  • Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
    • This allows skipping setting the InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
    • On X86 this also allows skipping using an EH prolog for this function
    • Only Update the MethodRegionInfo if there are EH regions to walk
  • Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
    • Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
    • Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checking it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
    • Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
    • For reflection emitted modules, always consider the status of this flag to be computed, but if we emit the controlling attribute, adjust the current cached computed value. NOTE: this is a minor breaking change from the previous behavior.
  • Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
  • Improve the performance of StackTraceInfo::AppendElement
    • It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
    • Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
    • Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
    • Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
  • Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
  • For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
  • Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work for better inlining behavior.

This PR improves the performance of deep stack EH throwing by micro-optimizing a number of scenarios. In particular in the Windows X86 Funclet model, it achieves about a 15% improvement to a simple benchmark.

…rticularly in the X86 with Funclets model
- Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
- This allows skipping setting hte InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
- On X86 this also allows skipping using an EH prolog for this function
- Only Update the MethodRegionInfo if there are EH regions to walk
- Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
- Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
- Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checkinging it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
- Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
- Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
- Improve the performance of StackTraceInfo::AppendElement
- It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
- Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
- Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
- Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
- Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
- For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
- Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work.
CopilotAI review requested due to automatic review settings April 11, 2025 23:24
@ghostghost added the area-ExceptionHandling-coreclr only use for closed issues label Apr 11, 2025

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.


class StackTraceInfo
{
struct StackTraceArrayProtect

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding inline documentation to clarify the roles of m_pStackTraceArray and m_pStackTraceArrayNew within StackTraceArrayProtect to improve future maintainability.

Copilot uses AI. Check for mistakes.
}
CONTRACTL_END

_ASSERTE(IsRuntimeWrapExceptionsStatusComputed());

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

It may be helpful to document the assumption that the runtime wrap exception status is computed before calling IsRuntimeWrapExceptionsDuringEH, to aid future maintainers in understanding the precondition.

Copilot uses AI. Check for mistakes.
GCPROTECT_BEGIN(newStackTrace);

size_t stackTraceCapacity = pStackTrace->Capacity();
size_t stackTraceCapacity = pStackTraceProtected->m_pStackTraceArray.Capacity();

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding a comment to explain the use of m_pStackTraceArray within the StackTraceArrayProtect structure, to clarify how capacity and copying are managed in the new implementation.

Copilot uses AI. Check for mistakes.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if !defined(DACCESS_COMPILE)

#ifdef DEBUGGING_SUPPORTED
static DebuggerExState m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
static DebuggerExState m_emptyDebuggerExState;
static DebuggerExState s_emptyDebuggerExState;

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if defined(_MSC_VER)
#pragma warning(default : 4640)
#endif
return &m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return &m_emptyDebuggerExState;
return &s_emptyDebuggerExState;

_ASSERTE(hdrInfoSize != 0);
m_hdrInfoTable = (PTR_CBYTE)gcInfoToken.Info + hdrInfoSize;
}
GCInfoToken gcInfoToken = GetGCInfoToken();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
GCInfoToken gcInfoToken = GetGCInfoToken();
_ASSERTE(m_hdrInfoTable == NULL);
GCInfoToken gcInfoToken = GetGCInfoToken();

@filipnavara

filipnavara commented Apr 13, 2025

Copy link
Copy Markdown
Member

Some of the test failures look legit:

> C:\h\w\A8C60910\w\A155092C\e\baseservices\baseservices\../callconvs/TestCallingConventions/TestCallingConventions.cmd
Assert failure(PID 3332 [0x00000d04], Thread: 1432 [0x0598]): pExInfo->m_pMDToReportFunctionLeave != NULL
CORECLR! NotifyExceptionPassStarted + 0x190 (0x00007fff`a273a730)
CORECLR! SfiInit + 0x128 (0x00007fff`a273d158)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c416d4)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c40cc8)
CORECLR! CallDescrWorkerInternal + 0x84 (0x00007fff`a2441f94)
CORECLR! CallDescrWorkerWithHandler + 0x120 (0x00007fff`a25bc5e8)
CORECLR! DispatchCallSimple + 0xF8 (0x00007fff`a25bce28)
CORECLR! DispatchManagedException + 0x238 (0x00007fff`a27387f8)
CORECLR! IL_Throw + 0x160 (0x00007fff`a2637b30)
<no module>! <no symbol> + 0x0 (0x00007fff`42f74094)
File: D:\a\_work\1\s\src\coreclr\vm\exceptionhandling.cpp:3680
Image: C:\h\w\A8C60910\p\corerun.exe

I didn't try to track it down yet, but it seems related to the NotifyFunctionEnter change.

Comment threadsrc/coreclr/vm/clrex.h Outdated
@filipnavara

Copy link
Copy Markdown
Member

Seems like the tests passed. Can we get this merged or is there still outstanding work to be done?

@davidwrighton
davidwrighton merged commit 2224669 into dotnet:mainApr 22, 2025
@filipnavara

Copy link
Copy Markdown
Member

Thanks a lot!

@davidwrighton

Copy link
Copy Markdown
MemberAuthor

@filipnavara I'm sorry about the delay, I've been unavoidably away from work for the last week, and I'm only now getting back. I didn't get a chance to look into the debugger scenarios, so I'll try to see if I can get that done some time this week, but I may not be able to.

@filipnavara

Copy link
Copy Markdown
Member

No worries. I appreciate any help with this! Thanks for keeping me in the loop.

mikem8361 pushed a commit to mikem8361/runtime that referenced this pull request Apr 24, 2025
This was caused by changes in PR dotnet#114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
mikem8361 pushed a commit that referenced this pull request Apr 25, 2025
)
This was caused by changes in PR #114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-ExceptionHandling-coreclronly use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@davidwrighton@filipnavara@jkotas@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model - #114582

Merged
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts
Apr 22, 2025
Merged

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model#114582
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts

Conversation

@davidwrighton

@davidwrightondavidwrighton commented Apr 11, 2025

Copy link
Copy Markdown
Member
  • Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
    • This allows skipping setting the InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
    • On X86 this also allows skipping using an EH prolog for this function
    • Only Update the MethodRegionInfo if there are EH regions to walk
  • Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
    • Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
    • Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checking it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
    • Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
    • For reflection emitted modules, always consider the status of this flag to be computed, but if we emit the controlling attribute, adjust the current cached computed value. NOTE: this is a minor breaking change from the previous behavior.
  • Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
  • Improve the performance of StackTraceInfo::AppendElement
    • It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
    • Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
    • Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
    • Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
  • Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
  • For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
  • Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work for better inlining behavior.

This PR improves the performance of deep stack EH throwing by micro-optimizing a number of scenarios. In particular in the Windows X86 Funclet model, it achieves about a 15% improvement to a simple benchmark.

…rticularly in the X86 with Funclets model
- Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
- This allows skipping setting hte InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
- On X86 this also allows skipping using an EH prolog for this function
- Only Update the MethodRegionInfo if there are EH regions to walk
- Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
- Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
- Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checkinging it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
- Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
- Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
- Improve the performance of StackTraceInfo::AppendElement
- It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
- Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
- Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
- Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
- Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
- For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
- Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work.
CopilotAI review requested due to automatic review settings April 11, 2025 23:24
@ghostghost added the area-ExceptionHandling-coreclr only use for closed issues label Apr 11, 2025

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.


class StackTraceInfo
{
struct StackTraceArrayProtect

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding inline documentation to clarify the roles of m_pStackTraceArray and m_pStackTraceArrayNew within StackTraceArrayProtect to improve future maintainability.

Copilot uses AI. Check for mistakes.
}
CONTRACTL_END

_ASSERTE(IsRuntimeWrapExceptionsStatusComputed());

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

It may be helpful to document the assumption that the runtime wrap exception status is computed before calling IsRuntimeWrapExceptionsDuringEH, to aid future maintainers in understanding the precondition.

Copilot uses AI. Check for mistakes.
GCPROTECT_BEGIN(newStackTrace);

size_t stackTraceCapacity = pStackTrace->Capacity();
size_t stackTraceCapacity = pStackTraceProtected->m_pStackTraceArray.Capacity();

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding a comment to explain the use of m_pStackTraceArray within the StackTraceArrayProtect structure, to clarify how capacity and copying are managed in the new implementation.

Copilot uses AI. Check for mistakes.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if !defined(DACCESS_COMPILE)

#ifdef DEBUGGING_SUPPORTED
static DebuggerExState m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
static DebuggerExState m_emptyDebuggerExState;
static DebuggerExState s_emptyDebuggerExState;

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if defined(_MSC_VER)
#pragma warning(default : 4640)
#endif
return &m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return &m_emptyDebuggerExState;
return &s_emptyDebuggerExState;

_ASSERTE(hdrInfoSize != 0);
m_hdrInfoTable = (PTR_CBYTE)gcInfoToken.Info + hdrInfoSize;
}
GCInfoToken gcInfoToken = GetGCInfoToken();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
GCInfoToken gcInfoToken = GetGCInfoToken();
_ASSERTE(m_hdrInfoTable == NULL);
GCInfoToken gcInfoToken = GetGCInfoToken();

@filipnavara

filipnavara commented Apr 13, 2025

Copy link
Copy Markdown
Member

Some of the test failures look legit:

> C:\h\w\A8C60910\w\A155092C\e\baseservices\baseservices\../callconvs/TestCallingConventions/TestCallingConventions.cmd
Assert failure(PID 3332 [0x00000d04], Thread: 1432 [0x0598]): pExInfo->m_pMDToReportFunctionLeave != NULL
CORECLR! NotifyExceptionPassStarted + 0x190 (0x00007fff`a273a730)
CORECLR! SfiInit + 0x128 (0x00007fff`a273d158)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c416d4)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c40cc8)
CORECLR! CallDescrWorkerInternal + 0x84 (0x00007fff`a2441f94)
CORECLR! CallDescrWorkerWithHandler + 0x120 (0x00007fff`a25bc5e8)
CORECLR! DispatchCallSimple + 0xF8 (0x00007fff`a25bce28)
CORECLR! DispatchManagedException + 0x238 (0x00007fff`a27387f8)
CORECLR! IL_Throw + 0x160 (0x00007fff`a2637b30)
<no module>! <no symbol> + 0x0 (0x00007fff`42f74094)
File: D:\a\_work\1\s\src\coreclr\vm\exceptionhandling.cpp:3680
Image: C:\h\w\A8C60910\p\corerun.exe

I didn't try to track it down yet, but it seems related to the NotifyFunctionEnter change.

Comment threadsrc/coreclr/vm/clrex.h Outdated
@filipnavara

Copy link
Copy Markdown
Member

Seems like the tests passed. Can we get this merged or is there still outstanding work to be done?

@davidwrighton
davidwrighton merged commit 2224669 into dotnet:mainApr 22, 2025
@filipnavara

Copy link
Copy Markdown
Member

Thanks a lot!

@davidwrighton

Copy link
Copy Markdown
MemberAuthor

@filipnavara I'm sorry about the delay, I've been unavoidably away from work for the last week, and I'm only now getting back. I didn't get a chance to look into the debugger scenarios, so I'll try to see if I can get that done some time this week, but I may not be able to.

@filipnavara

Copy link
Copy Markdown
Member

No worries. I appreciate any help with this! Thanks for keeping me in the loop.

mikem8361 pushed a commit to mikem8361/runtime that referenced this pull request Apr 24, 2025
This was caused by changes in PR dotnet#114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
mikem8361 pushed a commit that referenced this pull request Apr 25, 2025
)
This was caused by changes in PR #114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-ExceptionHandling-coreclronly use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@davidwrighton@filipnavara@jkotas@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model - #114582

Merged
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts
Apr 22, 2025
Merged

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model#114582
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts

Conversation

@davidwrighton

@davidwrightondavidwrighton commented Apr 11, 2025

Copy link
Copy Markdown
Member
  • Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
    • This allows skipping setting the InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
    • On X86 this also allows skipping using an EH prolog for this function
    • Only Update the MethodRegionInfo if there are EH regions to walk
  • Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
    • Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
    • Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checking it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
    • Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
    • For reflection emitted modules, always consider the status of this flag to be computed, but if we emit the controlling attribute, adjust the current cached computed value. NOTE: this is a minor breaking change from the previous behavior.
  • Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
  • Improve the performance of StackTraceInfo::AppendElement
    • It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
    • Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
    • Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
    • Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
  • Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
  • For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
  • Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work for better inlining behavior.

This PR improves the performance of deep stack EH throwing by micro-optimizing a number of scenarios. In particular in the Windows X86 Funclet model, it achieves about a 15% improvement to a simple benchmark.

…rticularly in the X86 with Funclets model
- Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
- This allows skipping setting hte InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
- On X86 this also allows skipping using an EH prolog for this function
- Only Update the MethodRegionInfo if there are EH regions to walk
- Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
- Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
- Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checkinging it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
- Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
- Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
- Improve the performance of StackTraceInfo::AppendElement
- It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
- Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
- Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
- Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
- Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
- For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
- Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work.
CopilotAI review requested due to automatic review settings April 11, 2025 23:24
@ghostghost added the area-ExceptionHandling-coreclr only use for closed issues label Apr 11, 2025

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.


class StackTraceInfo
{
struct StackTraceArrayProtect

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding inline documentation to clarify the roles of m_pStackTraceArray and m_pStackTraceArrayNew within StackTraceArrayProtect to improve future maintainability.

Copilot uses AI. Check for mistakes.
}
CONTRACTL_END

_ASSERTE(IsRuntimeWrapExceptionsStatusComputed());

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

It may be helpful to document the assumption that the runtime wrap exception status is computed before calling IsRuntimeWrapExceptionsDuringEH, to aid future maintainers in understanding the precondition.

Copilot uses AI. Check for mistakes.
GCPROTECT_BEGIN(newStackTrace);

size_t stackTraceCapacity = pStackTrace->Capacity();
size_t stackTraceCapacity = pStackTraceProtected->m_pStackTraceArray.Capacity();

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding a comment to explain the use of m_pStackTraceArray within the StackTraceArrayProtect structure, to clarify how capacity and copying are managed in the new implementation.

Copilot uses AI. Check for mistakes.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if !defined(DACCESS_COMPILE)

#ifdef DEBUGGING_SUPPORTED
static DebuggerExState m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
static DebuggerExState m_emptyDebuggerExState;
static DebuggerExState s_emptyDebuggerExState;

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if defined(_MSC_VER)
#pragma warning(default : 4640)
#endif
return &m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return &m_emptyDebuggerExState;
return &s_emptyDebuggerExState;

_ASSERTE(hdrInfoSize != 0);
m_hdrInfoTable = (PTR_CBYTE)gcInfoToken.Info + hdrInfoSize;
}
GCInfoToken gcInfoToken = GetGCInfoToken();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
GCInfoToken gcInfoToken = GetGCInfoToken();
_ASSERTE(m_hdrInfoTable == NULL);
GCInfoToken gcInfoToken = GetGCInfoToken();

@filipnavara

filipnavara commented Apr 13, 2025

Copy link
Copy Markdown
Member

Some of the test failures look legit:

> C:\h\w\A8C60910\w\A155092C\e\baseservices\baseservices\../callconvs/TestCallingConventions/TestCallingConventions.cmd
Assert failure(PID 3332 [0x00000d04], Thread: 1432 [0x0598]): pExInfo->m_pMDToReportFunctionLeave != NULL
CORECLR! NotifyExceptionPassStarted + 0x190 (0x00007fff`a273a730)
CORECLR! SfiInit + 0x128 (0x00007fff`a273d158)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c416d4)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c40cc8)
CORECLR! CallDescrWorkerInternal + 0x84 (0x00007fff`a2441f94)
CORECLR! CallDescrWorkerWithHandler + 0x120 (0x00007fff`a25bc5e8)
CORECLR! DispatchCallSimple + 0xF8 (0x00007fff`a25bce28)
CORECLR! DispatchManagedException + 0x238 (0x00007fff`a27387f8)
CORECLR! IL_Throw + 0x160 (0x00007fff`a2637b30)
<no module>! <no symbol> + 0x0 (0x00007fff`42f74094)
File: D:\a\_work\1\s\src\coreclr\vm\exceptionhandling.cpp:3680
Image: C:\h\w\A8C60910\p\corerun.exe

I didn't try to track it down yet, but it seems related to the NotifyFunctionEnter change.

Comment threadsrc/coreclr/vm/clrex.h Outdated
@filipnavara

Copy link
Copy Markdown
Member

Seems like the tests passed. Can we get this merged or is there still outstanding work to be done?

@davidwrighton
davidwrighton merged commit 2224669 into dotnet:mainApr 22, 2025
@filipnavara

Copy link
Copy Markdown
Member

Thanks a lot!

@davidwrighton

Copy link
Copy Markdown
MemberAuthor

@filipnavara I'm sorry about the delay, I've been unavoidably away from work for the last week, and I'm only now getting back. I didn't get a chance to look into the debugger scenarios, so I'll try to see if I can get that done some time this week, but I may not be able to.

@filipnavara

Copy link
Copy Markdown
Member

No worries. I appreciate any help with this! Thanks for keeping me in the loop.

mikem8361 pushed a commit to mikem8361/runtime that referenced this pull request Apr 24, 2025
This was caused by changes in PR dotnet#114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
mikem8361 pushed a commit that referenced this pull request Apr 25, 2025
)
This was caused by changes in PR #114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-ExceptionHandling-coreclronly use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@davidwrighton@filipnavara@jkotas@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model - #114582

Merged
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts
Apr 22, 2025
Merged

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model#114582
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts

Conversation

@davidwrighton

@davidwrightondavidwrighton commented Apr 11, 2025

Copy link
Copy Markdown
Member
  • Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
    • This allows skipping setting the InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
    • On X86 this also allows skipping using an EH prolog for this function
    • Only Update the MethodRegionInfo if there are EH regions to walk
  • Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
    • Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
    • Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checking it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
    • Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
    • For reflection emitted modules, always consider the status of this flag to be computed, but if we emit the controlling attribute, adjust the current cached computed value. NOTE: this is a minor breaking change from the previous behavior.
  • Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
  • Improve the performance of StackTraceInfo::AppendElement
    • It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
    • Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
    • Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
    • Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
  • Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
  • For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
  • Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work for better inlining behavior.

This PR improves the performance of deep stack EH throwing by micro-optimizing a number of scenarios. In particular in the Windows X86 Funclet model, it achieves about a 15% improvement to a simple benchmark.

…rticularly in the X86 with Funclets model
- Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
- This allows skipping setting hte InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
- On X86 this also allows skipping using an EH prolog for this function
- Only Update the MethodRegionInfo if there are EH regions to walk
- Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
- Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
- Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checkinging it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
- Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
- Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
- Improve the performance of StackTraceInfo::AppendElement
- It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
- Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
- Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
- Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
- Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
- For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
- Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work.
CopilotAI review requested due to automatic review settings April 11, 2025 23:24
@ghostghost added the area-ExceptionHandling-coreclr only use for closed issues label Apr 11, 2025

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.


class StackTraceInfo
{
struct StackTraceArrayProtect

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding inline documentation to clarify the roles of m_pStackTraceArray and m_pStackTraceArrayNew within StackTraceArrayProtect to improve future maintainability.

Copilot uses AI. Check for mistakes.
}
CONTRACTL_END

_ASSERTE(IsRuntimeWrapExceptionsStatusComputed());

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

It may be helpful to document the assumption that the runtime wrap exception status is computed before calling IsRuntimeWrapExceptionsDuringEH, to aid future maintainers in understanding the precondition.

Copilot uses AI. Check for mistakes.
GCPROTECT_BEGIN(newStackTrace);

size_t stackTraceCapacity = pStackTrace->Capacity();
size_t stackTraceCapacity = pStackTraceProtected->m_pStackTraceArray.Capacity();

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding a comment to explain the use of m_pStackTraceArray within the StackTraceArrayProtect structure, to clarify how capacity and copying are managed in the new implementation.

Copilot uses AI. Check for mistakes.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if !defined(DACCESS_COMPILE)

#ifdef DEBUGGING_SUPPORTED
static DebuggerExState m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
static DebuggerExState m_emptyDebuggerExState;
static DebuggerExState s_emptyDebuggerExState;

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if defined(_MSC_VER)
#pragma warning(default : 4640)
#endif
return &m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return &m_emptyDebuggerExState;
return &s_emptyDebuggerExState;

_ASSERTE(hdrInfoSize != 0);
m_hdrInfoTable = (PTR_CBYTE)gcInfoToken.Info + hdrInfoSize;
}
GCInfoToken gcInfoToken = GetGCInfoToken();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
GCInfoToken gcInfoToken = GetGCInfoToken();
_ASSERTE(m_hdrInfoTable == NULL);
GCInfoToken gcInfoToken = GetGCInfoToken();

@filipnavara

filipnavara commented Apr 13, 2025

Copy link
Copy Markdown
Member

Some of the test failures look legit:

> C:\h\w\A8C60910\w\A155092C\e\baseservices\baseservices\../callconvs/TestCallingConventions/TestCallingConventions.cmd
Assert failure(PID 3332 [0x00000d04], Thread: 1432 [0x0598]): pExInfo->m_pMDToReportFunctionLeave != NULL
CORECLR! NotifyExceptionPassStarted + 0x190 (0x00007fff`a273a730)
CORECLR! SfiInit + 0x128 (0x00007fff`a273d158)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c416d4)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c40cc8)
CORECLR! CallDescrWorkerInternal + 0x84 (0x00007fff`a2441f94)
CORECLR! CallDescrWorkerWithHandler + 0x120 (0x00007fff`a25bc5e8)
CORECLR! DispatchCallSimple + 0xF8 (0x00007fff`a25bce28)
CORECLR! DispatchManagedException + 0x238 (0x00007fff`a27387f8)
CORECLR! IL_Throw + 0x160 (0x00007fff`a2637b30)
<no module>! <no symbol> + 0x0 (0x00007fff`42f74094)
File: D:\a\_work\1\s\src\coreclr\vm\exceptionhandling.cpp:3680
Image: C:\h\w\A8C60910\p\corerun.exe

I didn't try to track it down yet, but it seems related to the NotifyFunctionEnter change.

Comment threadsrc/coreclr/vm/clrex.h Outdated
@filipnavara

Copy link
Copy Markdown
Member

Seems like the tests passed. Can we get this merged or is there still outstanding work to be done?

@davidwrighton
davidwrighton merged commit 2224669 into dotnet:mainApr 22, 2025
@filipnavara

Copy link
Copy Markdown
Member

Thanks a lot!

@davidwrighton

Copy link
Copy Markdown
MemberAuthor

@filipnavara I'm sorry about the delay, I've been unavoidably away from work for the last week, and I'm only now getting back. I didn't get a chance to look into the debugger scenarios, so I'll try to see if I can get that done some time this week, but I may not be able to.

@filipnavara

Copy link
Copy Markdown
Member

No worries. I appreciate any help with this! Thanks for keeping me in the loop.

mikem8361 pushed a commit to mikem8361/runtime that referenced this pull request Apr 24, 2025
This was caused by changes in PR dotnet#114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
mikem8361 pushed a commit that referenced this pull request Apr 25, 2025
)
This was caused by changes in PR #114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-ExceptionHandling-coreclronly use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@davidwrighton@filipnavara@jkotas@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model - #114582

Merged
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts
Apr 22, 2025
Merged

Micro optimizations to improve the performance of EH stackwalking, particularly in the X86 with Funclets model#114582
davidwrighton merged 6 commits into
dotnet:mainfrom
davidwrighton:X86FuncletPerfMicroOpts

Conversation

@davidwrighton

@davidwrightondavidwrighton commented Apr 11, 2025

Copy link
Copy Markdown
Member
  • Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
    • This allows skipping setting the InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
    • On X86 this also allows skipping using an EH prolog for this function
    • Only Update the MethodRegionInfo if there are EH regions to walk
  • Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
    • Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
    • Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checking it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
    • Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
    • For reflection emitted modules, always consider the status of this flag to be computed, but if we emit the controlling attribute, adjust the current cached computed value. NOTE: this is a minor breaking change from the previous behavior.
  • Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
  • Improve the performance of StackTraceInfo::AppendElement
    • It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
    • Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
    • Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
    • Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
  • Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
  • For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
  • Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work for better inlining behavior.

This PR improves the performance of deep stack EH throwing by micro-optimizing a number of scenarios. In particular in the Windows X86 Funclet model, it achieves about a 15% improvement to a simple benchmark.

…rticularly in the X86 with Funclets model
- Implement EHEnumInitFromStackFrameIterator as a SuppressGCTransition QCall and optimize its performance
- This allows skipping setting hte InlinedCallFrame to indicate that it is an EH frame (as suppress GC transition frames are NOT generated in that situation)
- On X86 this also allows skipping using an EH prolog for this function
- Only Update the MethodRegionInfo if there are EH regions to walk
- Improve the codegen and reduce the usage of UpdateRuntimeWrappedExceptions api
- Previously we would call IsRuntimeWrappedExceptions, which would lazily compute the flag. However, since we can't actually run the lazy computation during EH, we had already forced it to be initialized, so we didn't actually need to have the full lazy computation logic in place.
- Also, we were setting this flag as we walked the stack frame via SfiInit and SfiNext, but only checkinging it when parsing the EH clause data. Move the computation to the EHEnumInitFromStackFrameIterator api, and only compute the correct version of the flag IF there are clauses to walk.
- Only Update the IsRuntimeWrappedExceptions flag and the MethodRegionInfo if there are EH regions to walk
- Improve the performance of AppendExceptionStackFrame slightly be using the variant of GCX_COOP which takes a Thread* instead of getting it from the TLS data.
- Improve the performance of StackTraceInfo::AppendElement
- It always calls EnsureStackTraceArray which ALSO needs to have a protected GC variable. Instead of doing that locally in EnsureStackTraceArray, instead make the GCPROTECT in EnsureStackTraceArray be a bit larger. This allows avoiding modifying the TLS linked list of GCFrames, as well as avoids needing an x86 EH prolog for EnsureStackTraceArray
- Update ExceptionObject::GetStackTrace to use an out of line copy of code to clone the stack trace array in the presence of the multi-threaded scenario. This avoids the EH prolog on X86.
- Update ExceptionObject::GetStackTrace to avoid needing to regather the current thread, instead taking it as a parameter
- Change ExceptionObject::GetStackTraceParts to use a faster technique for checking to see if the array is an sbyte array or an object[].
- Update NotifyFunctionEnter to check CORProfileTrackExceptions before calling the various profiler reporting functions. This allows the check to happen only once instead of 4 times, and also allowed me to outline some logic so that the function didn't need an EH prolog on X86
- For handling of the m_emptyDebuggerExState on the ThreadExceptionState object, move that into a global static variable to improve the performance of calls of the ThreadExceptionState::GetDebuggerState api, and add a new ThreadExceptionState::SetDebuggerIndicatedFramePointer api to avoid even touching the empty debugger state
- Refactor the EECodeInfo::DecodeGCHdrInfo function into a fast inlineable path, and a slow path that does a lot of work.
CopilotAI review requested due to automatic review settings April 11, 2025 23:24
@ghostghost added the area-ExceptionHandling-coreclr only use for closed issues label Apr 11, 2025

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.


class StackTraceInfo
{
struct StackTraceArrayProtect

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding inline documentation to clarify the roles of m_pStackTraceArray and m_pStackTraceArrayNew within StackTraceArrayProtect to improve future maintainability.

Copilot uses AI. Check for mistakes.
}
CONTRACTL_END

_ASSERTE(IsRuntimeWrapExceptionsStatusComputed());

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

It may be helpful to document the assumption that the runtime wrap exception status is computed before calling IsRuntimeWrapExceptionsDuringEH, to aid future maintainers in understanding the precondition.

Copilot uses AI. Check for mistakes.
GCPROTECT_BEGIN(newStackTrace);

size_t stackTraceCapacity = pStackTrace->Capacity();
size_t stackTraceCapacity = pStackTraceProtected->m_pStackTraceArray.Capacity();

CopilotAIApr 11, 2025

Copy link

Choose a reason for hiding this comment

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

Consider adding a comment to explain the use of m_pStackTraceArray within the StackTraceArrayProtect structure, to clarify how capacity and copying are managed in the new implementation.

Copilot uses AI. Check for mistakes.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if !defined(DACCESS_COMPILE)

#ifdef DEBUGGING_SUPPORTED
static DebuggerExState m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
static DebuggerExState m_emptyDebuggerExState;
static DebuggerExState s_emptyDebuggerExState;

Comment threadsrc/coreclr/vm/exstate.cpp Outdated
#if defined(_MSC_VER)
#pragma warning(default : 4640)
#endif
return &m_emptyDebuggerExState;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return &m_emptyDebuggerExState;
return &s_emptyDebuggerExState;

_ASSERTE(hdrInfoSize != 0);
m_hdrInfoTable = (PTR_CBYTE)gcInfoToken.Info + hdrInfoSize;
}
GCInfoToken gcInfoToken = GetGCInfoToken();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
GCInfoToken gcInfoToken = GetGCInfoToken();
_ASSERTE(m_hdrInfoTable == NULL);
GCInfoToken gcInfoToken = GetGCInfoToken();

@filipnavara

filipnavara commented Apr 13, 2025

Copy link
Copy Markdown
Member

Some of the test failures look legit:

> C:\h\w\A8C60910\w\A155092C\e\baseservices\baseservices\../callconvs/TestCallingConventions/TestCallingConventions.cmd
Assert failure(PID 3332 [0x00000d04], Thread: 1432 [0x0598]): pExInfo->m_pMDToReportFunctionLeave != NULL
CORECLR! NotifyExceptionPassStarted + 0x190 (0x00007fff`a273a730)
CORECLR! SfiInit + 0x128 (0x00007fff`a273d158)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c416d4)
SYSTEM.PRIVATE.CORELIB! <no symbol> + 0x0 (0x00007fff`87c40cc8)
CORECLR! CallDescrWorkerInternal + 0x84 (0x00007fff`a2441f94)
CORECLR! CallDescrWorkerWithHandler + 0x120 (0x00007fff`a25bc5e8)
CORECLR! DispatchCallSimple + 0xF8 (0x00007fff`a25bce28)
CORECLR! DispatchManagedException + 0x238 (0x00007fff`a27387f8)
CORECLR! IL_Throw + 0x160 (0x00007fff`a2637b30)
<no module>! <no symbol> + 0x0 (0x00007fff`42f74094)
File: D:\a\_work\1\s\src\coreclr\vm\exceptionhandling.cpp:3680
Image: C:\h\w\A8C60910\p\corerun.exe

I didn't try to track it down yet, but it seems related to the NotifyFunctionEnter change.

Comment threadsrc/coreclr/vm/clrex.h Outdated
@filipnavara

Copy link
Copy Markdown
Member

Seems like the tests passed. Can we get this merged or is there still outstanding work to be done?

@davidwrighton
davidwrighton merged commit 2224669 into dotnet:mainApr 22, 2025
@filipnavara

Copy link
Copy Markdown
Member

Thanks a lot!

@davidwrighton

Copy link
Copy Markdown
MemberAuthor

@filipnavara I'm sorry about the delay, I've been unavoidably away from work for the last week, and I'm only now getting back. I didn't get a chance to look into the debugger scenarios, so I'll try to see if I can get that done some time this week, but I may not be able to.

@filipnavara

Copy link
Copy Markdown
Member

No worries. I appreciate any help with this! Thanks for keeping me in the loop.

mikem8361 pushed a commit to mikem8361/runtime that referenced this pull request Apr 24, 2025
This was caused by changes in PR dotnet#114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
mikem8361 pushed a commit that referenced this pull request Apr 25, 2025
)
This was caused by changes in PR #114582 where
Exception::GetStackTrace called GetThread() which throws an error exception in the
DAC. Changed the enummem.cpp code in ClrDataAccess::DumpManagedExcepObject() to call
the GetStackTrace overload that allows a NULL pCurrentThread parameter to be passed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-ExceptionHandling-coreclronly use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@davidwrighton@filipnavara@jkotas@AaronRobinsonMSFT