Description
When publishing with NativeAOT (PublishAot=true), the GCHeapAffinitizeRanges configuration is ignored regardless of how it is supplied — RuntimeHostConfigurationOption in the .csproj, IlcArg --runtimeopt:, or the DOTNET_GCHeapAffinitizeRanges environment variable. The Server GC heap threads fall back to the default per-heap affinity (heap 0 → CPU 0, heap 1 → CPU 1, etc.) instead of the user-specified ranges. The same configuration works correctly in non-AOT (JIT) builds. Other GC configs like GCHeapCount and gcServer are honored in NativeAOT, singling this out as specific to HeapAffinitizeRanges.
Reproduction Steps
- Create a minimal console app targeting net10.0 with PublishAot=true:
<!-- GCHeapAotBug.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.GC.HeapCount" Value="1" />
<RuntimeHostConfigurationOption Include="System.GC.HeapAffinitizeRanges" Value="7" />
</ItemGroup>
</Project>
// Program.cs
using System.Runtime;
using System.Runtime.InteropServices;
Console.WriteLine($"PID: {Environment.ProcessId}");
Console.WriteLine($"Server GC: {GCSettings.IsServerGC}");
// Keep GC active
var list = new List<byte[]>();
for (int i = 0; i < 60; i++)
{
list.Add(new byte[10 * 1024 * 1024]);
if (list.Count > 20) list.RemoveRange(0, 5);
if (i % 5 == 0) { GC.Collect(); GC.WaitForPendingFinalizers(); }
Thread.Sleep(1000);
}
- Publish and run:
dotnet publish -c Release -o ./publish
./publish/GCHeapAotBug &
PID=$!
cat /proc/$PID/task/*/status | grep -B1 Cpus_allowed_list
- Also confirmed that IlcArg --runtimeopt: and the DOTNET_GCHeapAffinitizeRanges env var both fail identically on the AOT binary.
Expected behavior
The .NET Server GC thread should have affinity restricted to CPU 7 (i.e. Cpus_allowed_list: 7), matching the configured GCHeapAffinitizeRanges=7 with GCHeapCount=1.
Actual behavior
The .NET Server GC thread ends up with Cpus_allowed_list: 0 — the default first-CPU binding. pidstat confirms the thread only ever runs on CPU 0, never CPU 7:
| TID |
NAME |
AFFINITY |
CPU |
| 2561246 |
.NET Server GC |
0 |
0 |
GCHeapCount=1 is honored (only 1 heap thread), proving configuration plumbing works. Only the affinity range is dropped.
Regression?
Unknown.
This is the first time testing this specific combination. It works correctly in non-AOT builds.
Known Workarounds
None.
Neither RuntimeHostConfigurationOption, IlcArg --runtimeopt:, nor DOTNET_GCHeapAffinitizeRanges nvironment variable overcome the issue on NativeAOT binaries. The only path is to not use NativeAOT.
Configuration
- .NET 10.0.102
- Ubuntu 22.04.5 LTS (WSL2, kernel 6.6.87.2)
- x64, 24 CPUs
- ServerGarbageCollection=true, PublishAot=true, RuntimeIdentifier=linux-x64
- The issue is specific to NativeAOT. Non-AOT (JIT) builds of the same code respect GCHeapAffinitizeRanges.
Other information
Related but not identical issues: #85961 (GC configs ignored in NativeAOT, fixed for HeapHardLimit) and #92458 some GC settings not honored, deferred to "Future" milestone). This appears to be an un-reported gap specific to the HeapAffinitizeRanges knob in the NativeAOT GC implementation.
Description
When publishing with NativeAOT (PublishAot=true), the GCHeapAffinitizeRanges configuration is ignored regardless of how it is supplied — RuntimeHostConfigurationOption in the .csproj, IlcArg --runtimeopt:, or the DOTNET_GCHeapAffinitizeRanges environment variable. The Server GC heap threads fall back to the default per-heap affinity (heap 0 → CPU 0, heap 1 → CPU 1, etc.) instead of the user-specified ranges. The same configuration works correctly in non-AOT (JIT) builds. Other GC configs like GCHeapCount and gcServer are honored in NativeAOT, singling this out as specific to HeapAffinitizeRanges.
Reproduction Steps
Expected behavior
The .NET Server GC thread should have affinity restricted to CPU 7 (i.e. Cpus_allowed_list: 7), matching the configured GCHeapAffinitizeRanges=7 with GCHeapCount=1.
Actual behavior
The .NET Server GC thread ends up with Cpus_allowed_list: 0 — the default first-CPU binding. pidstat confirms the thread only ever runs on CPU 0, never CPU 7:
GCHeapCount=1 is honored (only 1 heap thread), proving configuration plumbing works. Only the affinity range is dropped.
Regression?
Unknown.
This is the first time testing this specific combination. It works correctly in non-AOT builds.
Known Workarounds
None.
Neither RuntimeHostConfigurationOption, IlcArg --runtimeopt:, nor DOTNET_GCHeapAffinitizeRanges nvironment variable overcome the issue on NativeAOT binaries. The only path is to not use NativeAOT.
Configuration
Other information
Related but not identical issues: #85961 (GC configs ignored in NativeAOT, fixed for HeapHardLimit) and #92458 some GC settings not honored, deferred to "Future" milestone). This appears to be an un-reported gap specific to the HeapAffinitizeRanges knob in the NativeAOT GC implementation.