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>
| // this is the one of Ref_* function performed by single thread in MULTI_HEAPS case, so we need to loop through all HT of the bucket | ||
| for (int uCPUindex=0; uCPUindex < getNumberOfSlots(); uCPUindex++) | ||
| { | ||
| // this is the one of Ref_* function performed by single thread in MULTI_HEAPS case, so we need to loop through all HT of the bucket | ||
| for (int uCPUindex=0; uCPUindex < getNumberOfSlots(); uCPUindex++) | ||
| { | ||
| HHANDLETABLE hTable = walk->pBuckets[i]->pTable[uCPUindex]; | ||
| if (hTable) | ||
| HndScanHandlesForGC(hTable, VariableTraceDispatcher, | ||
| lp1, (uintptr_t)&info, &type, 1, condemned, maxgen, HNDGCF_EXTRAINFO | flags); | ||
| } | ||
| HHANDLETABLE hTable = bucket->pTable[uCPUindex]; | ||
| if (hTable) |
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "d0471714380924646dda8da4aeb1890f13141720",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "b2647f6fc4bf5a51e026f7dc880c9685b0947d13",
"last_reviewed_commit": "d0471714380924646dda8da4aeb1890f13141720",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "b2647f6fc4bf5a51e026f7dc880c9685b0947d13",
"last_recorded_worker_run_id": "29682994733",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "d0471714380924646dda8da4aeb1890f13141720",
"review_id": 4730589433
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: This is a follow-up to #128646, which removed the last producer of a multi-bucket / multi-AppDomain g_HandleTableMap layout in the GC handle-table code. Since coreclr removed AppDomain semantics from the GC in #24536 (May 2019), only g_HandleTableMap.pBuckets[0] is ever populated, pNext is permanently NULL, and dwMaxIndex is permanently INITIAL_HANDLE_TABLE_ARRAY_SIZE. The read side of the map was therefore dead scaffolding, and this PR removes it.
Approach: A small FORCEINLINE helper, GetGlobalHandleTableBucket(), returns g_HandleTableMap.pBuckets != NULL ? g_HandleTableMap.pBuckets[0] : NULL. Roughly 22 while (walk) { for (i in INITIAL_HANDLE_TABLE_ARRAY_SIZE) if (pBuckets[i]) ... walk = walk->pNext; } sites in objecthandle.cpp are collapsed to a single if (bucket != NULL) over that one bucket, and Ref_Shutdown / Ref_RemoveHandleTableBucket are simplified accordingly. handletable.cpp's HndCountAllHandles is collapsed to read pBuckets[0] directly. The on-disk HandleTableMap / HandleTableBucket DAC layout is deliberately preserved (the static_asserts at the bottom of objecthandle.cpp still hold), and the DAC/cDAC walking code is intentionally left for a separate follow-up. Ref_InitializeHandleTableBucket is intentionally untouched.
Summary: This is a faithful, mechanical simplification. Each rewritten site visits exactly the single bucket (and its getNumberOfSlots() slots) it always visited in practice, so there is no behavior change on the current-ABI configuration, and the loop-body scanning logic (slot striding via getSlotNumber/getThreadCount, lock acquisition in HndCountAllHandles, the single-thread MULTI_HEAPS loops) is preserved verbatim. The Ref_RemoveHandleTableBucket rewrite correctly reduces the old index-range search to HandleTableIndex == 0 && pBuckets[0] == pBucket, matching how buckets are assigned in Ref_InitializeHandleTableBucket (index always 0). The NULL-map guards (pBuckets != NULL) added throughout are appropriate for shutdown ordering. No GC<->DAC ABI change and no major_version_number bump, consistent with the preserved struct layout. The safety argument for dropping multi-bucket support is sound: no producer has existed since 2019, well before the last GC DAC ABI bump.
Verdict: LGTM. The change is a low-risk, self-consistent dead-code removal with clear rationale, and the deliberate deferral of the DAC/cDAC collapses is reasonable and clearly documented. I found no actionable correctness, performance, or convention issues in the changed lines.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 93.8 AIC · ⌖ 10.7 AIC · ⊞ 10K
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.