Skip to content

GCHeapAffinitizeRanges is completely ignored under NativeAOT #128396

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

Type

No type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions