ZeroGC: stop depending on MethodTable internal layout for IsLargeObject - #3298
Open
kkokosa wants to merge 2 commits into
Open
ZeroGC: stop depending on MethodTable internal layout for IsLargeObject#3298kkokosa wants to merge 2 commits into
kkokosa wants to merge 2 commits into
Conversation
added 2 commits
August 3, 2026 16:28
gcperfsim-cache (Workstation and Server GC modes) previously had no raw per-second dotnet-counters CSV capture in results/raw/, only the ZeroGC capture existed. This meant its TimeSeries.PauseTimePct chart data (stride- decimated to ~150 points from the full capture) could not be cross-checked or re-derived from source, and its reported PauseTimePctStats.Max included a rare one-off pause spike (11.80% Workstation) that the decimated chart series did not show (chart max ~0.46%), a ~26x visual understatement in report.html. This change re-runs gcperfsim-cache for both Workstation and Server GC for the same 600s duration as the rest of the suite, using an isolated output directory so the other 31 existing runs in results-full.json are left untouched, then merges just the two refreshed gcperfsim-cache entries back in and regenerates report.html (synced to docs/zerogc/report.html). Note: the new capture's Workstation run does not reproduce the prior rare pause spike (new Max 0.47% vs old 11.80%) -- GC pause spikes are inherently stochastic (e.g. one-off blocking Gen2 collection under memory pressure), so this is expected run-to-run noise, not a regression. Avg/P50/P90 for Workstation are consistent with the prior run (Avg 0.142% -> 0.120%, P50 0.149% -> 0.147%, P90 0.354% -> 0.350%). Full raw CSVs are now checked in for both modes, closing the previously-missing data gap.
…ject IsLargeObject(Object*) was the only place in ZeroGC that dereferenced a MethodTable's internal fields (via GetGCSafeMethodTable()->GetBaseSize()). MethodTable/Object's field layout (as mirrored in gcenv.object.h) is private VM implementation detail, not part of the versioned GC/EE interface (gcinterface.h's GC_INTERFACE_MAJOR/MINOR_VERSION) - it can change shape across runtime major versions with no interface version bump at all, which is exactly why ZeroGC.dll binaries are pinned to one target runtime major version. Since ZeroGC already segregates large/pinned objects into their own dedicated arena chunk at allocation time (the isLarge branch in AllocateFromArena), it already knows which objects are large without asking the object itself. This change records the exact address range of every allocation that meets the LOH size threshold (alignedSize >= LARGE_OBJECT_SIZE, matching the removed check's semantics precisely - not the broader isLarge flag, which also covers small pinned/POH allocations) into a small std::map<start, end> under a critical section, and answers IsLargeObject via a range lookup instead. This removes ZeroGC's only remaining runtime-internal-layout dependency for object introspection. The gcenv.*.h shim headers are still required (their Object*/MethodTable*/gc_alloc_context* type names appear throughout the mandatory IGCHeap/IGCHandleManager interface signatures - unavoidable for any standalone GC), but ZeroGC no longer dereferences any field inside MethodTable's actual struct layout anywhere, so that header's internal field-layout stability no longer matters for this GC's correctness. Verified: builds clean (native/build.ps1), and smoke-tested against GCPerfSim with -lohar/-lohsr/pinned-object args (exercises the LOH and POH allocation paths this change touches) - no crashes, collection_counts stay [0,0,0] as expected for ZeroGC.
Member
This is invalid statement. MethodTable data required to walk the object graph are a binary contract between the GC and EE for performance reasons. Regular shipping GC depends on this data heavily. Changing this data in incompatible way would definitely require major GC/EE interface version bump. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Removes ZeroGC's only dependency on MethodTable's internal field layout, in response to a question about whether ZeroGC breaks the standalone-GC design goal (per Maoni) of letting a newer GC binary run against an older runtime as long as possible.
Background
CoreCLR's actual GC/EE interface (
gcinterface.h'sGC_INTERFACE_MAJOR_VERSION/GC_INTERFACE_MINOR_VERSION) is explicitly designed for this: the EE only rejects a loaded GC whose reported major version is older than expected (gcheaputilities.cpp) - an equal-or-newer GC is accepted. That mechanism is untouched by ZeroGC and still works as designed.Where ZeroGC actually diverges: it's compiled directly against the VM's internal object-model headers (
gcenv.object.h'sMethodTable/Objectclasses, which mirror the real VM-internal layout byte-for-byte) rather than staying entirely within the versioned public interface surface. Those internal types have no separate version negotiation - they can change shape across runtime major versions with zero interface-version bump. Auditing the codebase, this was needed in exactly one place:ZeroGCHeap::IsLargeObject, which calledpObj->GetGCSafeMethodTable()->GetBaseSize().What changed
IsLargeObjectno longer touches the object'sMethodTableat all. ZeroGC already segregates large/pinned allocations into their own dedicated arena chunk at allocation time (see theisLargebranch inAllocateFromArena), so it already knows an allocation's large/small-ness without asking the object. This change:alignedSize >= LARGE_OBJECT_SIZE, matching the exact semantics of the removed check - deliberately narrower than the broaderisLargechunk-placement flag, which also covers small pinned/POH allocations that are not "large objects" by size) into astd::map<start, end>guarded by a critical section.IsLargeObjectnow does a range lookup (upper_bound) against that map instead of dereferencing the object'sMethodTable.What's still required (and why)
The
gcenv.*.hshim headers remain a hard dependency -Object*/MethodTable*/gc_alloc_context*type names appear throughout the mandatoryIGCHeap/IGCHandleManagerinterface signatures ZeroGC must implement, which is unavoidable for any standalone GC. What changed is that ZeroGC no longer dereferences any field insideMethodTable's actual struct layout anywhere in the codebase - so that header's internal field-layout stability (which is not covered by the GC interface's version contract) no longer affects ZeroGC's correctness.Testing
src/ZeroGC/native/build.ps1.-lohar/-lohsr(LOH) and pinned-object (pin every 100, POH) allocation args - exercises exactly the code path this change touches. No crashes;collection_counts: [0, 0, 0]as expected for ZeroGC.