Skip to content

[EXPERIMENTAL] Add opt-in FEATURE_2XPTR_ALIGNMENT for larger-than-pointer object payload alignment - #130885

Draft
tannergooding wants to merge 14 commits into
dotnet:mainfrom
tannergooding:tannergooding-gc-alignment-exploration
Draft

[EXPERIMENTAL] Add opt-in FEATURE_2XPTR_ALIGNMENT for larger-than-pointer object payload alignment#130885
tannergooding wants to merge 14 commits into
dotnet:mainfrom
tannergooding:tannergooding-gc-alignment-exploration

Conversation

@tannergooding

@tannergooding tannergooding commented Jul 16, 2026

Copy link
Copy Markdown
Member

What

Generalizes the existing 64-bit alignment support (FEATURE_64BIT_ALIGNMENT, historically only used on 32-bit ARM to force 8-byte alignment) into an opt-in compile-time feature, FEATURE_2XPTR_ALIGNMENT, that guarantees the payload of an object is aligned to 2 * sizeof(void*) (16 bytes on 64-bit). It is enabled via the CMake switch -DCLR_CMAKE_ENABLE_2XPTR_ALIGNMENT=1 and is OFF by default — a default 64-bit build is byte-identical to before (the rename is a no-op there).

This is intended as a testing/experimentation vehicle for larger-than-pointer alignment guarantees (e.g. validating Int128/Vector128 payload alignment behavior), not a shipping default.

Also adds inline allocation fast paths so the guarantee doesn't force every aligned allocation through the C++ slow path (amd64 Windows built + validated locally; arm64 .asm/.S and amd64-Unix .S are included but only CI can assemble/link them — I have no arm64/Unix toolchain locally).


Why it is not as costly as it looks

The alignment is applied to the object data (payload), not the object header. This is effectively malloc_aligned_at(objsize, 2*ptr, headersize) semantics — we align the point where the fields begin, not the allocation base.

Two consequences make the steady-state cost ~0 for the types that actually need it:

  1. Object size is unchanged. A boxed Int128/Vector128 is already 32 bytes on 64-bit because field layout places the 16-byte-aligned field at object offset 16 ([MT* @0][pad @8][field @16]). Vector256/Vector512 are already 48/80. The feature does not grow the object — it only forces the object's start address to be 2*ptr-aligned so that the offset-16 field lands on a 16-byte boundary.

  2. A packed run preserves phase. Because those objects are sized to a multiple of 16, a compacted, packed sequence of them keeps every payload 16-aligned with no gaps. The only padding is transient (fixing start-address phase at allocation / region boundaries) and the compacting GC reclaims it.


Compaction cost, and making it pay-for-play

The original writeup missed a secondary effect (thanks @jkotas): the first cut preserved 2 * DATA_ALIGNMENT residue for every plug during relocation — same_large_alignment_p compares only mod-16 residues, never the MethodTable — so a heap compiled with the feature compacted as if all objects were 16-aligned. On 64-bit ~half of ordinary objects sit at mod-16 == 8, so the compactor padded them on relocation and fragmentation ballooned.

This is now pay-for-play:

  • Per-plug: during plan, OR-fold RequiresAlign2xPtr() over each plug's objects into a plug_requires_large_align flag and gate every switch_alignment_size site on it. Plugs are rebuilt from survivors each GC, so the flag is self-clearing and defaults TRUE (safe over-preserve) until narrowed.
  • Per-region: a contains_large_align flag on heap_segment lets the plan phase skip the per-object probe for gen2 regions known clean. Gen2 regions receive objects only via GC placement or SIP, both of which set the flag, so a clear flag proves the region holds nothing to preserve.

compactbench: promote 4M survivors to gen2, drop every 3rd to fragment, then time one forced compacting gen2 GC. int128 legitimately needs 16-align; pair (16B, 8-align) and long (8B) are the non-aligned controls that must not be pessimized. windows.x64.Checked, workstation GC, concurrent off.

FragmentedBytes

type OFF global preserve pay-for-play
int128 128 4,216 176
pair 128 23,868,224 128
long 80 24,121,648 80

Worst case the global approach added ~+20–25% heap and ~+20–27% pause on a heap containing any aligned object; pay-for-play returns the non-aligned controls exactly to the OFF baseline while keeping int128 payloads 16-aligned. Full HeapSize / pause tables are in the comment below.


Numbers (windows.x64, gate-ON vs gate-OFF)

Object size (GetAllocatedBytesForCurrentThread, n=500k):

type gate-OFF gate-ON+fastpath delta
object / long (control) 24.000 24.000 0
Int128 / UInt128 / Vector128 32.000 32.094 +0.094
Vector256 48.000 48.142 +0.142
Vector512 80.000 80.236 +0.236

Live-heap footprint (heap_total_per_obj, after a compacting gen2 GC):

type gate-OFF gate-ON+fastpath
Int128 32.001 32.001
Vector128 32.001 32.001
Vector256 48.001 48.001
Vector512 80.001 80.001
Int128[] / Vector256[] base (identical) (identical)

Steady-state per-object heap cost is 0 — the ~0.1–0.24 B/alloc seen in the allocation counter is transient phase padding that compaction reclaims.

Allocation throughput (ns/alloc, FullOpts, DOTNET_TieredCompilation=0, n=500k):

type gate-OFF gate-ON, no fast path gate-ON + fast path
control_object 11.8 11.6 10.7
control_long 10.7 10.9 10.4
Int128 11.8 37.3 12.3
UInt128 12.2 37.4 13.1
Vector128 12.2 37.0 13.0
Vector256 13.3 42.1 13.1
Vector512 14.4 39.6 14.1

Without a fast path the guarantee costs ~+25 ns/alloc (the C++ slow path). The inline fast path removes it, returning aligned-box throughput to the unaligned baseline. Non-aligned allocations (controls) are unaffected either way.


Correctness

  • Alignment harness: all boxed/array 16-byte-payload types 500/500 aligned; compacting-GC relocation proof 2000/2000 objects moved and re-aligned, 0 corrupt.
  • GC test trees GC/API + GC/Coverage + GC/Regressions (29 runners), gate-ON with the pay-for-play changes:
    • no stress: 29/29 PASS
    • DOTNET_GCStress=0x3: 29/29 PASS
    • DOTNET_GCStress=0xC: 29/29 PASS

The full runtime GCStress suite has not been run locally (CI-scale).


Known limitations / out of scope

  • R2R: prebuilt ReadyToRun framework images encode gate-OFF (8-byte) type layouts, so Verify_TypeLayout rejects them under gate-ON. Tests here run with DOTNET_ReadyToRun=0; a gate-ON R2R rebuild is deferred.
  • arm64 + Unix asm fast paths are CI-validate-only (no local toolchain) — treat those files as unverified until CI assembles them.
  • No genuine inline array fast path (arrays re-derive the align flag from the MethodTable via the variable-size allocator, which is correct but slow-path).

Draft — for data/discussion, not merge.

Note

This PR description (and parts of the change) were drafted with GitHub Copilot.

tannergooding and others added 3 commits July 16, 2026 04:46
Rename the FEATURE_64BIT_ALIGNMENT concept and its `Align8`/`ALIGN8` naming to
`2xPtr`, reflecting that the guarantee is 2 * pointer-size object alignment
rather than a fixed 8 bytes. The x86 `double` hint stays gated on 2xPtr plus
!64-bit so its behavior is unchanged.

Add the compile-time gate FEATURE_2XPTR_ALIGNMENT (CMake
CLR_CMAKE_ENABLE_2XPTR_ALIGNMENT, default OFF) so the GC can guarantee 16-byte
payload alignment on 64-bit for types such as Int128/UInt128/Vector128. Value
types carry a DATA_ALIGNMENT bias so the payload -- not the object reference --
lands on the 2 * DATA_ALIGNMENT boundary. Default builds are byte-identical.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The scalar align helpers (NEWSFAST_ALIGN_2XPTR / _VC) defaulted to the
portable RhpNew backing on amd64, so every aligned box/newobj went through
the C++ RhpGcAlloc slow path (~25 ns/alloc over the unaligned baseline).
Mirror the 32-bit ARM inline-padding stubs on amd64: prepend a MIN_OBJECT_SIZE
dummy free object to flip the allocation-context phase when misaligned, else
bump inline. Wired via jitinterfacegen for TARGET_AMD64; the array helper
still routes through RhpNewVariableSizeObject (AllocateSzArray already aligns).
Gated on FEATURE_2XPTR_ALIGNMENT so default 64-bit builds are byte-identical.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mirrors the amd64 Windows fast path: RhpNewFastAlign2xPtr and
RhpNewFastMisalign inline the alignment-phase padding and only fall back
to RhpNewObject when the allocation context is in the wrong phase or the
object doesn't fit. Also fills the amd64 Unix gap where the prior fast
path was Windows-only.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 16:58
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 7 pipeline(s).
8 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@tannergooding

Copy link
Copy Markdown
Member Author

CC. @jkotas, @dotnet/gc

This is a rough draft (likely needs work, tuning, consideration) that extends the existing FEATURE_64BIT_ALIGNMENT to be FEATURE_2XPTR_ALIGNMENT so that it works on 64-bit and allows testing 16-byte alignment.

It has some early numbers and explanations of why this does not actually significantly impact heap size due to the use of the malloc_aligned_at like functionality (which I've raised on the past proposals/discussions around the topic).

I think this would be worth taking for .NET 12, still not enabled, and doing some more extensive A/B testing against real workloads, our perf benchmarks, and partner teams.

@tannergooding

tannergooding commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

It would notably pair nicely with an interlocked.compareexchange api handling 2x pointers, allow some AlignedPair<T, U> struct for reference types that could be used there as well

It also would bring .NET inline with essentially every other 64-bit memory allocator, which all systems we target default to 16-byte alignment for -- noting though that this prototype only does 16-byte alignment if the type actually needs it, such as using Int128 or Vector128 fields

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 introduces an opt-in FEATURE_2XPTR_ALIGNMENT (enabled via -DCLR_CMAKE_ENABLE_2XPTR_ALIGNMENT=1) and renames/propagates the prior “align8” concepts across CoreCLR GC/VM/JIT, cDAC contracts, debugger DAC/DBI interfaces, and NativeAOT plumbing so selected objects/arrays can be allocated with payload alignment of 2 * sizeof(void*).

Changes:

  • Rename “Align8” flags/APIs/helpers to “Align2xPtr” throughout CoreCLR (GC alloc flags, MethodTable flags, JIT helper selection) and diagnostics (cDAC + DacDbi).
  • Add/route aligned allocation helper implementations across multiple architectures (asm stubs / portable helper paths) and update helper enums/mappings.
  • Update the RuntimeTypeSystem cDAC contract docs and tests to match the renamed alignment query.
Show a summary per file
File Description
src/native/managed/cdac/tests/UnitTests/MethodTableTests.cs Updates unit test to validate renamed alignment flag/query.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs Renames DacDbi alignment query method to Align2xPtr.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs Implements Align2xPtr query by delegating to cDAC/legacy DAC.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs Renames the MethodTable flag/property to RequiresAlign2xPtr.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs Renames contract method to RequiresAlign2xPtr and updates ARM HFA shortcut.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs Renames calling-convention helper to RequiresAlign2xPtr.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs Renames the abstraction API to RequiresAlign2xPtr.
src/coreclr/vm/varargsnative.cpp Updates varargs alignment logic to use RequiresAlign2xPtr.
src/coreclr/vm/typehandle.h Renames TypeHandle alignment query under new feature guard.
src/coreclr/vm/typehandle.cpp Implements TypeHandle::RequiresAlign2xPtr.
src/coreclr/vm/runtimehandles.cpp Keeps fast-path allocator from handling aligned types.
src/coreclr/vm/readytoruninfo.cpp Updates ReadyToRun field-base offset alignment/bias logic for Align2xPtr.
src/coreclr/vm/object.h Updates packing guard to use FEATURE_2XPTR_ALIGNMENT.
src/coreclr/vm/object.cpp Updates object validation alignment assertion for Align2xPtr.
src/coreclr/vm/methodtablebuilder.h Renames builder “align8 candidate” setter to Align2xPtr.
src/coreclr/vm/methodtablebuilder.cpp Propagates Align2xPtr through layout/candidate detection and system type checks.
src/coreclr/vm/methodtable.h Renames RequiresAlign8/NativeRequiresAlign8 APIs and flag to Align2xPtr.
src/coreclr/vm/jitinterfacegen.cpp Installs new JIT helper entrypoints for Align2xPtr allocations (arch-dependent).
src/coreclr/vm/jitinterface.h Renames helper declarations for Align2xPtr allocation entrypoints.
src/coreclr/vm/jitinterface.cpp Updates helper selection and alignment requirement reporting for Align2xPtr.
src/coreclr/vm/i386/AsmMacros.inc Renames GC alloc flag constants to ALIGN_2XPTR.
src/coreclr/vm/gchelpers.cpp Updates object/array allocation flagging to request Align2xPtr from GC.
src/coreclr/vm/frozenobjectheap.cpp Rejects aligned types for frozen-object allocation.
src/coreclr/vm/classlayoutinfo.cpp Renames nested-field alignment tracking and extends Align2xPtr propagation logic.
src/coreclr/vm/class.h Renames nested layout flag and EEClass “prefer align” state to Align2xPtr.
src/coreclr/vm/class.cpp Renames and updates MethodTable native-alignment query to Align2xPtr.
src/coreclr/vm/callingconvention.h Updates ArgIterator alignment requirement query to RequiresAlign2xPtr.
src/coreclr/vm/arm64/asmmacros.h Defines GC alloc alignment flags for arm64 asm helpers.
src/coreclr/vm/arm64/asmconstants.h Adds ASM_MIN_OBJECT_SIZE constant/assert for asm fast paths.
src/coreclr/vm/amd64/AsmMacros.inc Defines GC alloc alignment flags for amd64 asm helpers.
src/coreclr/vm/amd64/asmconstants.h Adds ASM_MIN_OBJECT_SIZE constant/assert for asm fast paths.
src/coreclr/tools/Common/TypeSystem/Common/TypeSystemHelpers.cs Renames type-system helper to RequiresAlign2xPtr.
src/coreclr/tools/Common/TypeSystem/Common/TargetDetails.cs Renames target capability flag to SupportsAlign2xPtr.
src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs Threads new Align2xPtr naming through layout algorithm APIs.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Updates alignment requirement logic for Align2xPtr.
src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs Renames helper enum values to Align_2XPTR variants.
src/coreclr/tools/Common/Internal/Runtime/RuntimeConstants.cs Renames GC alloc flags constants to ALIGN_2XPTR.
src/coreclr/tools/Common/Internal/Runtime/MethodTable.Constants.cs Renames RequiresAlign8Flag to RequiresAlign2xPtrFlag.
src/coreclr/tools/Common/Internal/Runtime/EETypeBuilderHelpers.cs Emits RequiresAlign2xPtr flag for EEType generation.
src/coreclr/tools/Common/CallingConvention/ITypeHandle.cs Renames calling-convention interface method to RequiresAlign2xPtr.
src/coreclr/tools/Common/CallingConvention/ArgIterator.cs Updates calling-convention arg iterator to use RequiresAlign2xPtr.
src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs Updates NativeAOT RyuJIT helper mapping/selection for Align2xPtr.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunMetadataFieldLayoutAlgorithm.cs Threads Align2xPtr naming into R2R base-offset alignment logic.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs Updates R2R field-base offset calculation to use RequiresAlign2xPtr.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TypeHandle.cs Updates R2R type handle alignment queries to Align2xPtr.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs Updates preinit analysis failure tags from Align8 to Align2xPtr.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/JitHelper.cs Renames helper entrypoint selection strings to Align2xPtr variants.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs Updates comment to refer to RequiresAlign2xPtrFlag.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ThreadStaticsNode.cs Renames local alignment boolean and passes Align2xPtr requirement through.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs Updates GCStaticEEType caching key/logic to Align2xPtr naming.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NativeLayoutVertexNode.cs Updates GC/thread statics alignment derivation naming to Align2xPtr.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/GCStaticsNode.cs Updates statics alignment requirement naming to Align2xPtr.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DataOnlyEETypeNode.cs Renames alignment state and output name suffix to align2xptr.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ArrayOfFrozenObjectsNode.cs Updates assert to refer to RequiresAlign2xPtr.
src/coreclr/runtime/portable/AllocFast.cpp Renames aligned allocator entrypoints to Align2xPtr equivalents.
src/coreclr/runtime/arm64/AllocFast.S Adds arm64 asm fast path for aligned allocations under FEATURE_2XPTR_ALIGNMENT.
src/coreclr/runtime/arm64/AllocFast.asm Adds arm64 Windows asm fast path for aligned allocations under FEATURE_2XPTR_ALIGNMENT.
src/coreclr/runtime/arm/AllocFast.S Renames ARM32 aligned allocation stubs to Align2xPtr equivalents.
src/coreclr/runtime/amd64/AllocFast.S Adds amd64 Unix asm fast path for aligned allocations under FEATURE_2XPTR_ALIGNMENT.
src/coreclr/runtime/amd64/AllocFast.asm Adds amd64 Windows asm fast path for aligned allocations under FEATURE_2XPTR_ALIGNMENT.
src/coreclr/pal/inc/unixasmmacros.inc Renames GC alloc alignment flag constants to ALIGN_2XPTR.
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/FrozenObjectHeapManager.cs Updates NativeAOT frozen heap checks to new RequiresAlign2xPtr naming.
src/coreclr/nativeaot/Runtime/unix/unixasmmacros.inc Renames NativeAOT Unix asm GC alloc flags to ALIGN_2XPTR.
src/coreclr/nativeaot/Runtime/portable.cpp Renames NativeAOT portable allocation helpers/constants to Align2xPtr.
src/coreclr/nativeaot/Runtime/inc/MethodTable.h Renames NativeAOT RequiresAlign flag constant to RequiresAlign2xPtrFlag.
src/coreclr/nativeaot/Runtime/i386/AsmMacros.inc Renames NativeAOT i386 asm GC alloc flags to ALIGN_2XPTR.
src/coreclr/nativeaot/Runtime/arm64/AsmMacros.h Renames NativeAOT arm64 asm GC alloc flags to ALIGN_2XPTR.
src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/RuntimeExports.cs Updates NativeAOT runtime exports to use RequiresAlign2xPtr and new helper names.
src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/InternalCalls.cs Updates NativeAOT internal calls imports to Align2xPtr helper exports.
src/coreclr/nativeaot/Common/src/Internal/Runtime/MethodTable.cs Renames NativeAOT MethodTable property RequiresAlign2xPtr (and debug message).
src/coreclr/nativeaot/BuildIntegration/NativeAOT.natstepfilter Updates natstepfilter helper list to Align2xPtr names.
src/coreclr/jit/valuenum.cpp Updates VN helper classification list for new helper enum values.
src/coreclr/jit/utils.cpp Updates helper-call properties to recognize Align_2XPTR helper variants.
src/coreclr/jit/objectalloc.cpp Updates stackalloc array expansion to recognize Align_2XPTR helper.
src/coreclr/jit/importercalls.cpp Updates importer intrinsic handling for new array helper enum.
src/coreclr/jit/helperexpansion.cpp Updates helper expansion to include Align_2XPTR array helper.
src/coreclr/jit/gentree.cpp Updates helper-call handling to include Align_2XPTR array helper.
src/coreclr/inc/switches.h Defines FEATURE_2XPTR_ALIGNMENT for ARM/WASM and updates dependent feature guards.
src/coreclr/inc/jithelpers.h Renames helper identifiers used by the JIT/EE boundary to Align_2XPTR variants.
src/coreclr/inc/dacdbi.idl Renames DacDbi COM method to RequiresAlign2xPtr.
src/coreclr/inc/corinfo.h Renames JIT helper enum entries to Align_2XPTR variants.
src/coreclr/gc/interface.cpp Renames/updates GC aligned allocation routine to Align2xPtr semantics.
src/coreclr/gc/gcpriv.h Renames friend declaration for aligned allocation helper.
src/coreclr/gc/gcinternal.h Updates heap validation alignment check to RequiresAlign2xPtr.
src/coreclr/gc/gcinterface.h Renames GC alloc flags to ALIGN_2XPTR and updates comments.
src/coreclr/gc/env/gcenv.object.h Renames feature/flags/method names used by GC environment layer.
src/coreclr/debug/inc/dacdbiinterface.h Renames debug-side DacDbi interface method to RequiresAlign2xPtr.
src/coreclr/debug/di/rstype.cpp Renames CordbType alignment query and updates DAC call.
src/coreclr/debug/di/rspriv.h Renames CordbType alignment query declaration and its caller usage.
src/coreclr/debug/daccess/dacdbiimpl.h Renames DAC DacDbi implementation method to RequiresAlign2xPtr.
src/coreclr/debug/daccess/dacdbiimpl.cpp Renames DAC DacDbi implementation and updates RequiresAlign call.
src/coreclr/clrdefinitions.cmake Adds opt-in CMake switch to define FEATURE_2XPTR_ALIGNMENT (non-ARM/WASM).
docs/design/datacontracts/RuntimeTypeSystem.md Updates contract spec to reflect RequiresAlign2xPtr rename.

Copilot's findings

  • Files reviewed: 93/93 changed files
  • Comments generated: 4

Comment thread src/coreclr/gc/gcinternal.h
Comment thread src/coreclr/runtime/portable/AllocFast.cpp Outdated
Comment thread docs/design/datacontracts/RuntimeTypeSystem.md Outdated
@tannergooding tannergooding added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jul 16, 2026
@tannergooding tannergooding changed the title Add opt-in FEATURE_2XPTR_ALIGNMENT for larger-than-pointer object payload alignment [EXPERIMENTAL] Add opt-in FEATURE_2XPTR_ALIGNMENT for larger-than-pointer object payload alignment Jul 16, 2026
@jkotas

jkotas commented Jul 16, 2026

Copy link
Copy Markdown
Member

Why it is not as costly as it looks

This analysis looks incomplete to me. It ignores secondary effects like less efficient heap compaction with RESPECT_LARGE_ALIGNMENT enabled.

tannergooding and others added 3 commits July 16, 2026 10:22
…re-macro rename, refresh doc comments

- gcinternal.h: the object validation assert used the align8 mask/bias (0x7, 4U); on
  64-bit that would falsely fire for boxed value types under HeapVerify. Use
  2 * DATA_ALIGNMENT - 1 with a DATA_ALIGNMENT bias, matching Object::ValidateInner.
- Finish renaming FEATURE_64BIT_ALIGNMENT to FEATURE_2XPTR_ALIGNMENT: the base commit
  only renamed the CoreCLR side (switches.h/clrdefinitions.cmake) and left NativeAOT
  defining/consuming the old macro, so the shared runtime/portable/AllocFast.cpp aligned
  fast paths were compiled out on CoreCLR WASM. Rename the NativeAOT definitions
  (CMakeLists.txt, the csproj DefineConstants) and all remaining guards so there is a
  single macro. On the platforms NativeAOT defines it (32-bit arm/armel/wasm)
  2 * sizeof(void*) == 8, so the rename is behavior-preserving.
- Update the cDAC contract doc + spec comments to describe 2 * pointer-size alignment
  and FEATURE_2XPTR_ALIGNMENT instead of the stale 8-byte / FEATURE_64BIT_ALIGNMENT wording.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two spots touched by the rename still encoded the old 8-byte (32-bit) alignment
directly instead of the generalized 2 * pointer-size:

- classlayoutinfo.cpp GetFieldPlacementInfo floored a nested Align2xPtr field's
  alignment with max(8u, ...). On a 64-bit opt-in build the floor should be 16.
  In practice GetFieldAlignmentRequirement already reports 16 for the real
  larger-than-pointer types, so this is a consistency/robustness fix rather than
  an observable bug, but it now matches methodtablebuilder.cpp (2 * DATA_ALIGNMENT).
- runtime/portable/AllocFast.cpp aligned fast paths used sizeof(int64_t) for the
  phase check, sizeof(int32_t) for the bias, and a truncating (uint32_t) cast.
  These only coincide with 2 * pointer-size on the 32-bit targets that currently
  compile the portable helpers (WASM for CoreCLR; arm/armel/wasm for NativeAOT),
  so behavior is unchanged there; using 2 * sizeof(void*) / sizeof(void*) makes
  the ...Align2xPtr helpers correct by construction and drops the 64-bit-unsafe cast.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…e comments

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 17:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 96/96 changed files
  • Comments generated: 3

Comment thread src/coreclr/jit/objectalloc.cpp
Comment thread src/coreclr/vm/gchelpers.cpp Outdated
Comment thread src/coreclr/jit/utils.cpp Outdated
@tannergooding

Copy link
Copy Markdown
Member Author

This analysis looks incomplete to me. It ignores secondary effects like less efficient heap compaction with RESPECT_LARGE_ALIGNMENT enabled.

Ah... I see that we're pessimizing all plugs if any large object exists so yeah that can add a decent amount of fragmentation in worst case scenarios (effectively equivalent to making all objects 16B aligned).

We recompute plugs every GC though, so we can trivially add a has_large_align flag and make this a per plug, rather than per-object cost and win most of that back. Going to prototype it.

Looks like it wouldn't be terrible to do a per-region check of "last GC this region had some large_align" object either, it just wouldn't be live for this GC due to ordering. So I might follow up with that.

Neither of those should be overly complex or intrusive and wouldn't require a big rewrite to support.

same_large_alignment_p compares only mod-DATA_ALIGNMENT residues, so the
compactor preserves accidental alignment for every plug even on heaps with no
2 * DATA_ALIGNMENT object. On 64-bit ~half of ordinary objects land at
mod-16 == 8, so this padded a large fraction of survivors for nothing.

Narrow the residue-preservation padding to plugs that actually need it. During
plan, OR-fold RequiresAlign2xPtr() over each plug's objects into a per-plug
plug_requires_large_align flag and gate every switch_alignment_size site on it.
Plugs are rebuilt from survivors each GC, so the flag is self-clearing and
defaults TRUE (safe over-preserve) until narrowed.

Layer a per-region contains_large_align flag on top so the plan phase can skip
the per-object MethodTable probe for gen2 regions known to be clean. Gen2
regions receive objects only via GC placement or SIP, both of which set the
flag, so a clear flag on a gen2 region proves it holds nothing to preserve;
ephemeral regions are always probed. The flag is cleared on region reset and in
sweep_region_in_plan (recomputed from in-place survivors), keeping it
conservative -- never wrongly FALSE.

Local smoke tests (windows.x64.Checked, DOTNET_gcConcurrent=0):
- Pessimistic workload (4M gen2 survivors, no aligned object): global
  preservation regressed pair/long fragmentation to ~24MB and GC time +25-30%;
  pay-for-play returns both to the OFF baseline (pair 128B, long 80B).
- Aligned workload stays correct: Int128 payloads 16-aligned, relocation path
  2000/2000 objects moved and 16-aligned, 0 corrupt.
- gcstress 0x3 and 0xC: GC API/Coverage/Regressions (29 tests) pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 19:16
@tannergooding

Copy link
Copy Markdown
Member Author

This analysis looks incomplete to me. It ignores secondary effects like less efficient heap compaction with RESPECT_LARGE_ALIGNMENT enabled.

Right -- that was a real gap, and a large one. The first cut preserved 2 * DATA_ALIGNMENT residue for every plug during relocation (same_large_alignment_p compares only mod-16 residues, never the MethodTable), so a heap compiled with the feature compacted as if all objects were 16-aligned. On 64-bit ~half of ordinary objects sit at mod-16 == 8, so the compactor padded them on relocation and fragmentation ballooned.

I made it pay-for-play (9f0816de2b3):

  • Per-plug: during plan, OR-fold RequiresAlign2xPtr() over each plug's objects into a plug_requires_large_align flag and gate every switch_alignment_size site on it. Plugs are rebuilt from survivors each GC, so the flag is self-clearing.
  • Per-region: a contains_large_align flag on heap_segment lets the plan phase skip the per-object probe for gen2 regions known clean -- gen2 regions only receive objects via GC placement or SIP, both of which set the flag, so a clear flag proves there's nothing to preserve.

compactbench promotes 4M survivors to gen2, drops every 3rd to fragment, then times one forced compacting gen2 GC. int128 legitimately needs 16-align; pair (16B, 8-align) and long (8B) are the non-aligned controls that must not be pessimized. windows.x64.Checked, workstation GC, concurrent off.

FragmentedBytes

type OFF global preserve per-plug per-plug + region
int128 128 4,216 176 176
pair 128 23,868,224 128 128
long 80 24,121,648 80 80

HeapSizeBytes

type OFF global preserve per-plug per-plug + region
int128 117,570,728 117,574,840 117,570,800 117,570,800
pair 117,570,720 141,438,840 (+20.3%) 117,570,744 117,570,744
long 96,237,344 120,358,936 (+25.1%) 96,237,368 96,237,368

Compacting gen2 GC time (ms)

type OFF global preserve per-plug per-plug + region
int128 225.9 226.3 226.8 229.9
pair 223.1 282.3 (+26.5%) 224.2 224.8
long 239.0 287.3 (+20.2%) 220.9 222.5

So the secondary effect was real -- worst case ~+20-25% heap and ~+20-27% pause on a heap containing any aligned object. Pay-for-play returns the non-aligned controls (pair, long) exactly to the OFF baseline while keeping int128 payloads 16-aligned (176 B fragmentation, pause within noise). The per-region layer matches per-plug on outcomes at this scale; its value is structural -- it skips the O(objects) MethodTable probe on huge clean gen2 heaps, and self-clears for SIP regions -- but the probe it removes is already in the noise at 4M objects, so I'd take it only for the large-heap insurance and the self-clearing property.

gcstress re-validated on the pay-for-play build: GC/API + GC/Coverage + GC/Regressions (29 runners) pass under no-stress, 0x3, and 0xC; the relocation harness still moves 2000/2000 objects 16-aligned, 0 corrupt.

Note

This comment was drafted with GitHub Copilot.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 100/100 changed files
  • Comments generated: 2

Comment thread src/coreclr/runtime/portable/AllocFast.cpp Outdated
Comment thread src/coreclr/jit/objectalloc.cpp
@tannergooding

Copy link
Copy Markdown
Member Author

#130885 (comment)

Not a perfect bench, but massively reduces the worst case scenarios and should make it more A/B testable. Still some cases you could end up with, like perfectly interleaving aligned and unaligned objects, but I expect that's rare in practice due to the limited number of types that can be 16-byte aligned and the scenarios they get used in. Might be more impactful if we did it for all arrays, but I expect the per-plug/region setup should still keep it minimized and could be refined over time.

tannergooding and others added 2 commits July 16, 2026 13:04
…paths

The dummy free object used to reverse alignment was formatted with only its
MethodTable set, leaving its length field uninitialized. Set the component
count to 0 to match the amd64 asm fast paths, which write m_Length = 0.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Odd-rank multi-dimensional value-type arrays (e.g. Int128[,,]) have a payload
that sits one pointer past a 2 * pointer-size boundary, so aligning the object
start is not enough. AllocateArrayEx now requests a biased header via
GC_ALLOC_ALIGN_2XPTR_BIAS in that case; previously the code asserted the payload
was always a whole multiple of the alignment and mis-aligned (or asserted) for
odd ranks. The SZARRAY path keeps its exact assert since its payload phase is
always zero.

On 64-bit the JIT has no mechanism to give a single frame local greater-than-
pointer-size alignment (lvStructDoubleAlign only reaches 8 bytes and is 32-bit
only), so stack allocating a 2xPtr array would under-align its payload. Bail out
of array stack allocation for CORINFO_HELP_NEWARR_1_ALIGN_2XPTR on 64-bit and
leave the array on the heap where the alignment is honored.

Also correct the LOH comment: biased headers are unsupported there, so objects
reaching the LOH fall back to 8-byte alignment even when 2 * pointer-size was
requested (e.g. a large Int128[]).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 20:06
utils.cpp and FrozenObjectHeapManager.cs each had their leading BOM removed as a
side effect of unrelated edits, adding encoding-only diff noise. Restore it to
keep the diff focused on the alignment changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (1)

src/coreclr/gc/interface.cpp:1544

  • The UOH/LOH allocation path does not honor GC_ALLOC_ALIGN_2XPTR: allocate_uoh_object always AlignQword's the size and asserts only 8-byte alignment ("& 7"). With FEATURE_2XPTR_ALIGNMENT enabled on 64-bit, types/arrays requesting 16-byte alignment via GC_ALLOC_ALIGN_2XPTR can still end up only 8-byte aligned when they cross the LOH threshold. If the feature is meant to guarantee 2*ptr payload alignment, this is a correctness gap; consider teaching the UOH allocator to honor GC_ALLOC_ALIGN_2XPTR (at least for the non-biased case) or explicitly preventing aligned allocations from going to UOH.
        // The LOH always guarantees at least 8-byte alignment, regardless of platform. It doesn't support
        // mis-aligned (biased) object headers, so a biased header must never be requested for an object that
        // lands here: boxed value types can never grow large enough to reach the LOH, and the array allocators
        // only ever request a bias on the small object heap. As a consequence, objects that reach the LOH fall
        // back to 8-byte alignment even when 2 * pointer-size alignment was requested (e.g. a large Int128[]).
        ASSERT((flags & GC_ALLOC_ALIGN_2XPTR_BIAS) == 0);
        ASSERT(65536 < loh_size_threshold);

        int gen_num = (flags & GC_ALLOC_PINNED_OBJECT_HEAP) ? poh_generation : loh_generation;
        newAlloc = (Object*) hp->allocate_uoh_object (size + ComputeMaxStructAlignPadLarge(requiredAlignment), flags, gen_num, acontext->alloc_bytes_uoh);
        ASSERT(((size_t)newAlloc & 7) == 0);
  • Files reviewed: 100/100 changed files
  • Comments generated: 1

Comment thread src/coreclr/debug/di/rspriv.h Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 20:16
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 100/100 changed files
  • Comments generated: 2

Comment thread src/coreclr/gc/plan_phase.cpp Outdated
Comment thread src/coreclr/vm/gchelpers.cpp Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 20:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

tannergooding and others added 2 commits July 16, 2026 14:03
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
AllocateObject set the 2xPtr alignment flags unconditionally, so a large boxed
value type requiring 2 * pointer-size alignment reached the LOH with the bias
flag set and tripped the no-bias assert in GCHeap::Alloc. Guard the flags to the
small object heap, matching the array allocators; large objects fall back to
8-byte alignment.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 21:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 100/100 changed files
  • Comments generated: 2

Comment on lines 402 to +405
/// Determines whether an object of type '<paramref name="type"/>' requires 8-byte alignment on
/// 32bit ARM or 32bit Wasm architectures.
/// </summary>
public static bool RequiresAlign8(this TypeDesc type)
public static bool RequiresAlign2xPtr(this TypeDesc type)
Comment on lines 365 to +369
/// <summary>
/// Encapsulates the fact that some architectures require 8-byte (larger than pointer
/// size) alignment on some value types and arrays.
/// </summary>
public bool SupportsAlign8
public bool SupportsAlign2xPtr
…acts

Preserve upstream type-handle, layout, and allocation-helper changes while retaining experimental twice-pointer alignment. Update moved compiler logic, cDAC contracts, auxiliary symbols, and Decimal128 alignment.

Validation is source-level only: the clean-head clr+libs+host baseline failed with MSVC C1083 in NativeAOT datadescriptor.cpp. Builds and tests remain blocked; this local merge is committed with that gap explicitly accepted.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-GC-coreclr NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants