Align the data of frozen arrays#131347
Conversation
Every object on the large object heap already has a padding object in front of it (it is where LOH compaction keeps the relocation distance of the object). Grow that padding so that the data of the object - where the payload of an array starts, sizeof(ArrayBase) into the object - ends up aligned on a 64 byte boundary. This makes vectorized operations over large arrays cheaper since their elements no longer straddle cache lines. The padding is computed from the address it is placed at, so allocations at the end of a segment/region as well as allocations out of the free list stay aligned. LOH compaction picks aligned destinations too; since the gap in front of an object is no longer a fixed size, plan_loh remembers the extra padding of an object in the low 3 bits of its relocation distance (which is always a multiple of 8) so that compact_loh can make a free object out of the whole gap. Objects that are pinned don't move and keep using the gap size the plan phase recorded for them. Note that a string keeps its characters 4 bytes before where an array keeps its data on 64 bit and objects are only aligned on a pointer boundary, so the characters of a string end up 4 bytes short of the boundary. The alignment can be changed or turned off with DOTNET_GCLOHDataAlignment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b36af726-9140-4b8a-bbf9-e124ad795e15
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @anicka-net, @dotnet/gc |
There was a problem hiding this comment.
Pull request overview
This PR changes CoreCLR GC LOH allocation / compaction to optionally insert additional per-object padding so that the array data start lands on a configurable (default 64-byte) alignment boundary, and adds a GC test to validate alignment for LOH arrays / strings across allocations, free-list reuse, and LOH compaction.
Changes:
- Add a new GC config (
GCLOHDataAlignment) and plumb it through GC initialization to drive LOH “data alignment” padding behavior. - Update LOH planning, relocation, and UOH allocation paths to compute and preserve variable padding (including encoding extra alignment padding into LOH relocation metadata during compaction).
- Add a new GC test project validating data alignment for LOH arrays and the expected skew for strings.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tests/GC/Features/LOHCompaction/lohalignment.csproj | New test project for validating LOH data alignment behavior. |
| src/tests/GC/Features/LOHCompaction/lohalignment.cs | Adds alignment validation for arrays/strings across new alloc, free-list alloc, and LOH compaction. |
| src/coreclr/gc/relocate_compact.cpp | Applies variable LOH padding during compaction and asserts post-move alignment. |
| src/coreclr/gc/plan_phase.cpp | Computes alignment-aware LOH padding during planning and records extra pad for relocation. |
| src/coreclr/gc/interface.cpp | Initializes loh_data_alignment from the new GC config and validates accepted values. |
| src/coreclr/gc/gcpriv.h | Updates method signatures to pass alignment padding info where needed. |
| src/coreclr/gc/gcinternal.h | Adds alignment helpers/state and encodes extra alignment padding in LOH relocation metadata. |
| src/coreclr/gc/gcconfig.h | Introduces the GCLOHDataAlignment configuration key. |
| src/coreclr/gc/gc.cpp | Defines the new global loh_data_alignment. |
| src/coreclr/gc/allocation.cpp | Adjusts LOH/UOH allocation to reserve/track alignment padding and validate aligned results. |
Extend the alignment to the pinned object heap (the padding logic is the same, POH objects just don't have the LOH's relocation padding in front of them) and rename the LOH-specific names to UOH ones, including the config which is now GCUOHDataAlignment. Do the same in FrozenObjectHeapManager (both the C++ and the C# copy) for arrays of at least 512 bytes - smaller ones are not worth the padding. As on the UOH the gap is formatted as a free object so the segment stays walkable; the profiler's object enumerator skips over those. Also trim the comments and remove the test - CI covers this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b36af726-9140-4b8a-bbf9-e124ad795e15
Padding objects on the LOH required threading the variable gap size through the plan and compact phases and stealing 3 bits of the relocation distance to do it. That is a lot of machinery for one heap, so drop it and keep the two heaps where the padding is just an allocation-time decision: * POH, which is never compacted, so the gap only has to be made at allocation time (GCPOHDataAlignment) * the frozen object heap, for arrays of at least 512 bytes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b36af726-9140-4b8a-bbf9-e124ad795e15
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
src/coreclr/vm/frozenobjectheap.cpp:276
- HasComponentSize() is true for strings and arrays (incl. multidimensional arrays). Using it here together with ARRAYBASE_SIZE means large strings / MD arrays can get padded using the SZARRAY data offset, which won’t actually align their payload as intended. Restrict to arrays and compute the data offset from the MethodTable base size (works for MD arrays too).
if ((objectSize < FOH_MIN_SIZE_TO_ALIGN) || !type->HasComponentSize())
{
return 0;
}
const size_t dataAddr = (size_t)m_pCurrent + ARRAYBASE_SIZE;
size_t pad = ALIGN_UP(dataAddr, (size_t)FOH_DATA_ALIGNMENT) - dataAddr;
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/FrozenObjectHeapManager.cs:234
- Like CoreCLR, HasComponentSize also includes strings. Padding based on ArrayBaseSize will not align string payloads (and it also won’t match MD-array element offsets). If the intent is to align array element data, restrict to IsArray and compute the data offset from BaseSize (covers MD arrays).
if ((objectSize < FOH_MIN_SIZE_TO_ALIGN) || !type->HasComponentSize)
{
return 0;
}
nuint dataAddr = (nuint)m_pCurrent + ArrayBaseSize;
nuint pad = (FOH_DATA_ALIGNMENT - (dataAddr & (FOH_DATA_ALIGNMENT - 1))) & (FOH_DATA_ALIGNMENT - 1);
src/coreclr/gc/gcinternal.h:1219
- The comment says this aligns “the elements of an array”, but poh_data_offset() is hardcoded to sizeof(ArrayBase), which is only the element-data offset for SZ arrays. Strings and MD arrays have different data offsets, and POH allocations aren’t type-specific at this layer. Please tighten the wording to reflect what is actually being aligned.
// Objects on the POH are padded so that their data - sizeof(ArrayBase) into the object,
// where the elements of an array start - is aligned on this boundary. 0 disables it.
extern size_t poh_data_alignment;
Padding on the POH touches the shared UOH allocation paths (the free list and the end of a segment) which the LOH goes through as well, so it isn't as isolated as it looks. Keep just the frozen object heap, where the padding is local to FrozenObjectSegment::TryAllocateObject and its object walk. The GC is left untouched by this PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b36af726-9140-4b8a-bbf9-e124ad795e15
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
src/coreclr/vm/frozenobjectheap.cpp:273
HasComponentSize()is true for both arrays and strings (see MethodTable::IsStringOrArray), but the padding math usesARRAYBASE_SIZE, which is array-specific. This will also insert padding in front of large frozen strings without actually guaranteeing string buffer alignment, increasing memory usage with no clear benefit. Consider restricting this to arrays (or handle strings separately with the correct string data offset).
if ((objectSize < FOH_MIN_SIZE_TO_ALIGN) || !type->HasComponentSize())
{
return 0;
}
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/FrozenObjectHeapManager.cs:230
HasComponentSizeincludes strings as well as arrays, butArrayBaseSizeis an array layout constant. This would apply padding to large strings too (and won’t ensure string buffer alignment). If the intent is array-only alignment, restrict this check totype->IsArray.
if ((objectSize < FOH_MIN_SIZE_TO_ALIGN) || !type->HasComponentSize)
{
return 0;
}
* Use ArrayBase::GetDataPtrOffset and the free object's own base size instead of hardcoding the layout of an array * Pad arrays only, not every object with a component size. A string keeps its characters at an offset that is never 8 byte aligned, so no amount of padding can put them on a cache line - it would be pure waste * Fold the free object skipping in the object walk into one helper * Align on the widest vector the platform can load, so 16 rather than 64 on arm64, and do it for arrays of 128 bytes and up Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b36af726-9140-4b8a-bbf9-e124ad795e15
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
src/coreclr/vm/frozenobjectheap.cpp:18
- The PR title/description talk about aligning objects on the LOH to 64 bytes, but this change is in FrozenObjectHeap (FOH) and also sets FOH_DATA_ALIGNMENT to 16 on Arm64. Please reconcile scope and alignment guarantees (update PR title/description, or adjust the implementation/constant naming to match the stated goal).
#ifdef TARGET_ARM64
#define FOH_DATA_ALIGNMENT 16
#else
#define FOH_DATA_ALIGNMENT 64
#endif
Keep one alignment and one threshold for all platforms rather than tuning them per architecture. 512 bytes keeps the worst case - an array whose size lands just past the threshold and needs the largest gap - at 14% of the array. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b36af726-9140-4b8a-bbf9-e124ad795e15
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
src/coreclr/vm/frozenobjectheap.cpp:14
- FOH_DATA_ALIGNMENT is hardcoded to 64, but the PR description claims arm64 should use 16 (since Vector128 is the widest load there). Either update the description or make the alignment platform-conditional so the implementation matches the stated intent.
// The data of arrays of at least FOH_MIN_SIZE_TO_ALIGN bytes is put on a cache line. The
// threshold keeps the gap we leave in front of an array small relative to the array.
#define FOH_DATA_ALIGNMENT 64
#define FOH_MIN_SIZE_TO_ALIGN 512
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/FrozenObjectHeapManager.cs:36
- FOH_DATA_ALIGNMENT is hardcoded to 64 here as well, which contradicts the PR description’s claim that arm64 should use 16-byte alignment. Either update the description or make this constant platform-conditional to keep CoreCLR and NativeAOT behavior aligned with the stated intent.
// The data of arrays of at least FOH_MIN_SIZE_TO_ALIGN bytes is put on a cache
// line. The threshold keeps the gap we leave in front of an array small relative
// to the array.
private const nuint FOH_DATA_ALIGNMENT = 64;
private const nuint FOH_MIN_SIZE_TO_ALIGN = 512;
| // The MethodTable the GC uses to mark a range of memory as unused, it is what the | ||
| // gaps we leave to align the data of large arrays are formatted as. | ||
| private static readonly MethodTable* s_freeObjectMethodTable = | ||
| (MethodTable*)RuntimeImports.RhGetFreeObjectMethodTable(); | ||
|
|
|
@EgorBot -arm -amd -intel using BenchmarkDotNet.Attributes;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
public class Benchs
{
static readonly int[] data = new int[1024];
[Benchmark]
public int Bench() => SumSafe(data);
[MethodImpl(MethodImplOptions.NoInlining)]
public static int SumSafe(Span<int> span)
{
int sum = 0;
if (Vector512.IsHardwareAccelerated)
{
var acc512 = Vector512<int>.Zero;
while (span.Length > Vector512<int>.Count)
{
acc512 += Vector512.Create(span);
span = span.Slice(Vector512<int>.Count);
}
sum = Vector512.Sum(acc512);
}
foreach (int value in span)
sum += value;
return sum;
}
} |
| private const nuint FOH_DATA_ALIGNMENT = 64; | ||
| private const nuint FOH_MIN_SIZE_TO_ALIGN = 512; |
There was a problem hiding this comment.
Would it be possible to align smaller objects to 16 instead?
There was a problem hiding this comment.
Probably? didn't want to overcomplicate
|
I think it's likely not worth as is
|
Just opus-5 experiment:
Align the data of arrays on the frozen object heap so that vectorized code doesn't have its loads straddle cache lines.
FOH objects are laid out back to back, so where an array's data lands is arbitrary. This leaves a gap in front of arrays of at least 128 bytes so that their first element starts on a
FOH_DATA_ALIGNMENTboundary - 64 bytes, or 16 on arm64 whereVector128is the widest load. The gap is formatted as a free object so the segment stays walkable, and the object walk used byEnumModuleFrozenObjectsskips those the same waygc_heap::walk_heapdoes.Strings are left alone: their characters sit at an offset that is never 8 byte aligned, so padding can't put them on a cache line anyway.
The change is duplicated in the NativeAOT copy of
FrozenObjectHeapManager, which needed a way to get at the free objectMethodTable(RhGetFreeObjectMethodTable).Cost is up to one alignment plus a free object header per array, and only for arrays that are frozen (
static readonly), so it is bounded and paid once at startup.Benchmark
See #131347 (comment)
the static readonly array is allocated on FOH, intel and amd targets reported 20% perf boost.