Skip to content

gc: collapse single-bucket handle table walks#128718

Open
max-charlamb wants to merge 2 commits into
dotnet:mainfrom
max-charlamb:gc/collapse-handle-table-walks
Open

gc: collapse single-bucket handle table walks#128718
max-charlamb wants to merge 2 commits into
dotnet:mainfrom
max-charlamb:gc/collapse-handle-table-walks

Conversation

@max-charlamb
Copy link
Copy Markdown
Member

Follow-up to #128646. After that PR collapsed the multi-bucket / multi-AppDomain producer in Ref_Initialize and friends, the read side of g_HandleTableMap is dead scaffolding: only pBuckets[0] is ever populated, pNext is permanently NULL, and dwMaxIndex is permanently INITIAL_HANDLE_TABLE_ARRAY_SIZE.

This PR collapses those dead walks in the GC internals to single-bucket access via a tiny helper:

static FORCEINLINE HandleTableBucket* GetGlobalHandleTableBucket()
{
    return g_HandleTableMap.pBuckets != NULL ? g_HandleTableMap.pBuckets[0] : NULL;
}

Scope

  • src/coreclr/gc/objecthandle.cpp: ~22 walk sites collapsed; Ref_Shutdown and Ref_RemoveHandleTableBucket simplified. Ref_InitializeHandleTableBucket deliberately untouched (reachable through the IGCHandleManager::CreateHandleStore virtual; not worth re-engineering here).
  • src/coreclr/gc/handletable.cpp: HndCountAllHandles collapsed. Reads g_HandleTableMap.pBuckets[0] directly -- same source of truth the DAC sees -- to avoid a layering inversion through GCHandleStore.

Non-goals

  • No GC<->DAC ABI change. gcDacVars->handle_table_map and the dac_handle_table_map struct layout (pBuckets, pNext, dwMaxIndex) are intact. No major_version_number bump.
  • No DAC or cDAC changes. The walking patterns in daccess.cpp (DacHandleWalker and the SOS memory region collector) and in cDAC GC_1 (GetHandles, GetHandleTableMemoryRegions) are left untouched. Those collapses are mechanically straightforward (~50 lines net) and can land in a separate follow-up if reviewers want them.

Why this is safe

Multi-bucket has had no producer in coreclr since #24536 ("Remove concept of AppDomains from the GC", May 2019) -- almost 4 years before the most recent GC DAC major_version_number bump (1 -> 2 in #84454, April 2023). Any standalone GC targeting the current ABI cannot have legitimately produced a multi-bucket layout: the AppDomain semantics it would have modeled aren't exposed by coreclr anymore.

Validation

  • build.cmd clr.runtime -c Checked: clean.
  • No behavior change: every walk site now visits exactly the single bucket it always visited in practice.

Note

This PR was drafted with assistance from GitHub Copilot.

Follow-up to dotnet#128646. After that PR the multi-bucket / multi-AppDomain
handle table scaffolding has no producer: only g_HandleTableMap.pBuckets[0]
is ever populated, pNext is permanently null, and dwMaxIndex is permanently
the array size constant. Collapse the dead walk patterns in objecthandle.cpp
and the one in handletable.cpp to single-bucket form via a small helper.

No DAC/cDAC ABI change -- the on-disk HandleTableMap layout is preserved
for gcDacVars->handle_table_map and the cDAC GC_1 reader. The walking
patterns in legacy DAC (daccess.cpp) and cDAC (GC_1.cs) are left alone in
this change and can be simplified in a separate follow-up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 28, 2026 19:04
@max-charlamb max-charlamb added the NO-REVIEW Experimental/testing PR, do NOT review it label May 28, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

struct HandleTableMap * walk = &g_HandleTableMap;
// After collapsing the legacy multi-bucket / multi-AppDomain handle-table-map
// scaffolding, there is at most a single bucket slot at index 0.
HandleTableBucket* bucket = g_HandleTableMap.pBuckets != NULL ? g_HandleTableMap.pBuckets[0] : NULL;
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.

What is the benefit of keeping the bucket array with single entry instead of just a pointer to a bucket?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removing the top level HandleTableBucket would break the GC<->EE API, we can do this but it would require a major GC version bump.

I guess we could get rid of it on the GC side entirely and fake it on the interface, but this also seems confusing. My thought was to simplify what we can then remove it entirely the next time we do a major GC version bump.

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.

I am not sure how it would break it. The g_HandleTableMap seems to be accessed only at the GC side of things.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sorry I mean the gcinterfacedacvars.def versioning. This pointer is accessed by the DAC and would require shimming or a break in the supported GC versions

I see three options:

  1. Break and bump major to 3. Cleanest end state, but it severs compat with every older standalone GC / older DAC pairing. The last break was 3 years ago so I don't really want to do another.
  2. Shim the old slot. Keep g_HandleTableMap as a thin compatibility wrapper that still exposes the old dac_handle_table_map layout (one bucket array of length 1 pointing atg_pMainHandleTableBucket, pNext = nullptr, dwMaxIndex = 1), and additionally add handle_table_bucket at the end of the dacvars struct behind a minor bump. New DAC/cDAC read the new field; old DACs keep working against the shim. Most backcompat-preserving, modest extra code.
  3. Drop the GC-side refactor entirely and keep the change scoped to DAC/cDAC walk simplification only (treat the existing map as effectively single-bucket on the read side). Zero compat risk, but loses the GC simplification.

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.

Ah, that makes sense. Let's keep your change as is then.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@max-charlamb max-charlamb marked this pull request as ready for review June 4, 2026 17:04
Copilot AI review requested due to automatic review settings June 4, 2026 17:04
Copy link
Copy Markdown
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 2 out of 2 changed files in this pull request and generated 4 comments.

Comment on lines +33 to +38
// g_HandleTableMap.pBuckets[0] is the only ever-populated bucket; the map is
// retained for DAC layout (gcDacVars->handle_table_map).
static FORCEINLINE HandleTableBucket* GetGlobalHandleTableBucket()
{
return g_HandleTableMap.pBuckets != NULL ? g_HandleTableMap.pBuckets[0] : NULL;
}
Comment on lines 738 to +739
// destroy the handle table bucket array
HandleTableMap *walk = &g_HandleTableMap;
while (walk) {
delete [] walk->pBuckets;
walk = walk->pNext;
}
delete [] g_HandleTableMap.pBuckets;
Comment on lines +843 to +847
if (g_HandleTableMap.pBuckets != NULL &&
pBucket->HandleTableIndex == 0 &&
g_HandleTableMap.pBuckets[0] == pBucket)
{
if ((index < walk->dwMaxIndex) && (index >= offset))
{
// During AppDomain unloading, we first remove a handle table and then destroy
// the table. As soon as the table is removed, the slot can be reused.
if (walk->pBuckets[index - offset] == pBucket)
{
walk->pBuckets[index - offset] = NULL;
return;
}
}
offset = walk->dwMaxIndex;
walk = walk->pNext;
g_HandleTableMap.pBuckets[0] = NULL;
Comment on lines +996 to +998
HandleTableBucket* bucket = g_HandleTableMap.pBuckets != NULL ? g_HandleTableMap.pBuckets[0] : NULL;
if (bucket == NULL)
return 0;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants