Skip to content

GCHeapAffinitizeRanges is completely ignored under NativeAOT #128396

@yang-xiaodong

Description

@yang-xiaodong

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

  1. 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);
  }
  1. Publish and run:
  dotnet publish -c Release -o ./publish
  ./publish/GCHeapAotBug &
  PID=$!
  cat /proc/$PID/task/*/status | grep -B1 Cpus_allowed_list
  1. 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.

Metadata

Metadata

Assignees

Labels

area-GC-coreclruntriagedNew issue has not been triaged by the area owner

Type

No type
No fields configured for issues without a type.

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions