gc: collapse single-bucket handle table walks#128718
Conversation
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>
|
Tagging subscribers to this area: @JulieLeeMSFT, @dotnet/gc |
| 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; |
There was a problem hiding this comment.
What is the benefit of keeping the bucket array with single entry instead of just a pointer to a bucket?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I am not sure how it would break it. The g_HandleTableMap seems to be accessed only at the GC side of things.
There was a problem hiding this comment.
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:
- 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.
- 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.
- 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.
There was a problem hiding this comment.
Ah, that makes sense. Let's keep your change as is then.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // 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; | ||
| } |
| // destroy the handle table bucket array | ||
| HandleTableMap *walk = &g_HandleTableMap; | ||
| while (walk) { | ||
| delete [] walk->pBuckets; | ||
| walk = walk->pNext; | ||
| } | ||
| delete [] g_HandleTableMap.pBuckets; |
| 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; |
| HandleTableBucket* bucket = g_HandleTableMap.pBuckets != NULL ? g_HandleTableMap.pBuckets[0] : NULL; | ||
| if (bucket == NULL) | ||
| return 0; |
Follow-up to #128646. After that PR collapsed the multi-bucket / multi-AppDomain producer in
Ref_Initializeand friends, the read side ofg_HandleTableMapis dead scaffolding: onlypBuckets[0]is ever populated,pNextis permanentlyNULL, anddwMaxIndexis permanentlyINITIAL_HANDLE_TABLE_ARRAY_SIZE.This PR collapses those dead walks in the GC internals to single-bucket access via a tiny helper:
Scope
src/coreclr/gc/objecthandle.cpp: ~22 walk sites collapsed;Ref_ShutdownandRef_RemoveHandleTableBucketsimplified.Ref_InitializeHandleTableBucketdeliberately untouched (reachable through theIGCHandleManager::CreateHandleStorevirtual; not worth re-engineering here).src/coreclr/gc/handletable.cpp:HndCountAllHandlescollapsed. Readsg_HandleTableMap.pBuckets[0]directly -- same source of truth the DAC sees -- to avoid a layering inversion throughGCHandleStore.Non-goals
gcDacVars->handle_table_mapand thedac_handle_table_mapstruct layout (pBuckets,pNext,dwMaxIndex) are intact. Nomajor_version_numberbump.daccess.cpp(DacHandleWalkerand the SOS memory region collector) and in cDACGC_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_numberbump (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.Note
This PR was drafted with assistance from GitHub Copilot.