Skip to content

Replace GCX_COOP with EBR for EEHashTable bucket reclamation#124822

Draft
AaronRobinsonMSFT wants to merge 3 commits intodotnet:mainfrom
AaronRobinsonMSFT:ebr-eehash
Draft

Replace GCX_COOP with EBR for EEHashTable bucket reclamation#124822
AaronRobinsonMSFT wants to merge 3 commits intodotnet:mainfrom
AaronRobinsonMSFT:ebr-eehash

Conversation

@AaronRobinsonMSFT
Copy link
Member

@AaronRobinsonMSFT AaronRobinsonMSFT commented Feb 24, 2026

Replace cooperative GC mode transitions (GCX_COOP_NO_THREAD_BROKEN) with Epoch-Based Reclamation for safe deferred deletion of old EEHashTable bucket arrays during resize.

Changes

  • Add dedicated g_EEHashEbr collector (separate from HashMap's g_HashMapEbr), with initialization in ceemain.cpp and cleanup in f inalizerthread.cpp
  • Add AllocateEEHashBuckets FreeEEHashBuckets helpers in eehash.cpp, following the same pattern as HashMap's AllocateBuckets FreeBuckets
  • Remove SyncClean::AddEEHashTable and associated cleanup infrastructure from syncclean.cpp/.hpp
  • Remove redundant MemoryBarrier() before VolatilePtr::Store() — release semantics already provide the ordering guarantee

Follow-up to #124307 (Replace HashMap COOP transitions with EBR). Specifically see #124307 (comment).

@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

@AaronRobinsonMSFT AaronRobinsonMSFT added this to the 11.0.0 milestone Feb 24, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR replaces the prior GC cooperative-mode based scheme for safely reclaiming obsolete EEHashTable bucket arrays with Epoch-Based Reclamation (EBR), aligning EEHashTable resizing behavior with the earlier HashMap EBR work.

Changes:

  • Add a dedicated global EBR collector (g_EEHashEbr) and initialize it during EE startup; trigger cleanup from the finalizer thread’s “extra work” loop.
  • Refactor EEHashTable bucket allocation/freeing into helpers and use EBR critical regions + deferred deletion during table growth instead of GCX_COOP_NO_THREAD_BROKEN + SyncClean.
  • Simplify SyncClean by removing the EEHashTable-specific cleanup list and related APIs.

Reviewed changes

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

Show a summary per file
File Description
src/coreclr/vm/syncclean.hpp Removes EEHashTable cleanup list API; leaves SyncClean::CleanUp() only.
src/coreclr/vm/syncclean.cpp Removes EEHashTable cleanup list storage and freeing logic.
src/coreclr/vm/finalizerthread.cpp Adds g_EEHashEbr cleanup request detection and processing in finalizer extra-work path.
src/coreclr/vm/eehash.inl Replaces COOP transitions with EBR critical regions; defers old bucket array deletion via EBR during growth.
src/coreclr/vm/eehash.h Declares EEHashTable bucket allocation/free helpers.
src/coreclr/vm/eehash.cpp Implements AllocateEEHashBuckets / FreeEEHashBuckets.
src/coreclr/vm/ebr.h Declares extern EbrCollector g_EEHashEbr.
src/coreclr/vm/ebr.cpp Defines global g_EEHashEbr.
src/coreclr/vm/ceemain.cpp Initializes g_EEHashEbr during EE startup.

Replace cooperative GC mode transitions (GCX_COOP_NO_THREAD_BROKEN) with
Epoch-Based Reclamation for safe deferred deletion of old EEHashTable
bucket arrays during resize.

- Add dedicated g_EEHashEbr collector (init, cleanup, critical regions)
- Add AllocateEEHashBuckets/FreeEEHashBuckets helpers in eehash.cpp
- Remove SyncClean::AddEEHashTable and associated cleanup infrastructure
- Remove redundant MemoryBarrier before VolatilePtr::Store (release semantics)

Co-authored-by: Copilot <[email protected]>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.


InterlockedExchange( (LONG *) &m_bGrowing, 0);

// Before EE starts we're single-threaded; delete old buckets immediately.
Copy link
Member

Choose a reason for hiding this comment

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

Is this a correct statement? We do create finalizer thread, debugger threads, tracing threads, ... before g_fEEStarted is set to true. Is it guaranteed that none of those threads cross the path with this?

The per-thread EbrThreadData is a single thread_local with one m_pCollector
field, so it can only be associated with one collector at a time. Having
two collectors caused the same TLS node to be linked into both thread
lists, corrupting epoch tracking and thread detach cleanup.

Both HashMap and EEHashTable use generic byte-array reclamation, so a
single shared collector is sufficient. Added an assertion in
GetOrCreateThreadData to guard against future reintroduction of multiple
collectors.

Co-authored-by: Copilot <[email protected]>
Replace CrstLeafLock usage in AsyncContinuationsManager with a
dedicated CrstAsyncContinuations crst type that has proper lock
ordering (AcquiredBefore LeafLock).

Co-authored-by: Copilot <[email protected]>
Copilot AI review requested due to automatic review settings February 26, 2026 02:07
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment on lines +738 to +742
// Before EE starts we're single-threaded; delete old buckets immediately.
if (!g_fEEStarted)
{
FreeEEHashBuckets(pOldBuckets);
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The comment and logic assume single-threaded execution before g_fEEStarted, but the finalizer thread, debugger threads, and other system threads are created before g_fEEStarted is set to TRUE (see ceemain.cpp lines 871, 940, 854). If these threads could access EEHashTable instances during this period, the immediate deletion of old buckets here would be unsafe.

This pattern was inherited from the old SyncClean::AddEEHashTable implementation, but given that EEHashTable always allows lock-free reads (unlike HashMap which has an m_fAsyncMode flag), it may be safer to always use EBR for deferred deletion rather than conditional logic based on g_fEEStarted.

Consider either: (1) always queueing for EBR deletion regardless of g_fEEStarted, or (2) adding documentation/assertions that EEHashTable operations cannot occur from multiple threads before g_fEEStarted.

Suggested change
// Before EE starts we're single-threaded; delete old buckets immediately.
if (!g_fEEStarted)
{
FreeEEHashBuckets(pOldBuckets);
}
// Always retire old buckets via EBR to ensure safety for lock-free readers.
EBR::DeleteLater(DeleteObsoleteEEHashBuckets, pOldBuckets);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants