From 538f0956239fde50df4dbafa43f404d0d1bc3f0e Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Fri, 28 Aug 2026 02:59:10 +0500 Subject: [PATCH 1/4] allow affinity mask wider than 32 processors the affinity cli option was parsed as int so a mask with any bit above 32 could not be passed at all even though the job stores it as IntPtr and FixAffinity already handles a 64 bit mask parsing it as long also means the perfonar model must hold long or the value gets truncated on the way out --- .../ConsoleArguments/CommandLineOptions.cs | 2 +- src/BenchmarkDotNet/Jobs/EnvironmentMode.cs | 2 +- src/BenchmarkDotNet/Models/BdnEnvironment.cs | 2 +- .../ConfigParserTests.cs | 20 +++++++++++++++++++ .../Perfonar/PerfonarTests.cs | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index cc06b11216..17a4969e8e 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -64,7 +64,7 @@ public bool UseDisassemblyDiagnoser public OutlierMode Outliers { get; set; } [Option("affinity", Required = false, HelpText = "Affinity mask to set for the benchmark process")] - public int? Affinity { get; set; } + public long? Affinity { get; set; } [Option("allStats", Required = false, Default = false, HelpText = "Displays all statistics (min, max & more)")] public bool DisplayAllStatistics { get; set; } diff --git a/src/BenchmarkDotNet/Jobs/EnvironmentMode.cs b/src/BenchmarkDotNet/Jobs/EnvironmentMode.cs index 636dcfc35e..b8515879d2 100644 --- a/src/BenchmarkDotNet/Jobs/EnvironmentMode.cs +++ b/src/BenchmarkDotNet/Jobs/EnvironmentMode.cs @@ -148,7 +148,7 @@ internal Runtime GetRuntime() { Jit = HasValue(JitCharacteristic) ? Jit : null, Runtime = HasValue(RuntimeCharacteristic) ? Runtime?.RuntimeMoniker : null, - Affinity = HasValue(AffinityCharacteristic) ? (int)Affinity : null + Affinity = HasValue(AffinityCharacteristic) ? (long)Affinity : null }; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Models/BdnEnvironment.cs b/src/BenchmarkDotNet/Models/BdnEnvironment.cs index 1256d83c6f..1c56fa7e13 100644 --- a/src/BenchmarkDotNet/Models/BdnEnvironment.cs +++ b/src/BenchmarkDotNet/Models/BdnEnvironment.cs @@ -8,5 +8,5 @@ internal class BdnEnvironment : EnvironmentInfo { public RuntimeMoniker? Runtime { get; set; } public Jit? Jit { get; set; } - public int? Affinity { get; set; } + public long? Affinity { get; set; } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index a735ff4d99..7147cb77db 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -495,6 +495,26 @@ public void PackagesPathParsedCorrectly() Assert.Equal(fakeRestoreDirectory, ((DotNetCliGenerator)toolchain.Generator).PackagesPath); } + [Fact] + public void UserCanSpecifyAffinity() + { + const long affinity = 0b1010; + var config = ConfigParser.Parse(["--affinity", affinity.ToString()], new OutputLogger(Output)).config; + + Assert.NotNull(config); + Assert.Equal(new IntPtr(affinity), config.GetJobs().Single().Environment.Affinity); + } + + [Fact] + public void UserCanSpecifyAffinityBeyondThirtyTwoProcessors() + { + const long affinity = 1L << 40; + var config = ConfigParser.Parse(["--affinity", affinity.ToString()], new OutputLogger(Output)).config; + + Assert.NotNull(config); + Assert.Equal(new IntPtr(affinity), config.GetJobs().Single().Environment.Affinity); + } + [Fact] public void UserCanSpecifyBuildTimeout() { diff --git a/tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs b/tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs index 81d35e488d..f8931012d8 100644 --- a/tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs +++ b/tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs @@ -145,7 +145,7 @@ public Task PerfonarTableTest(string key) ] }; - private static EntryInfo Job(RuntimeMoniker? runtime = null, Jit? jit = null, int? affinity = null) => new EntryInfo + private static EntryInfo Job(RuntimeMoniker? runtime = null, Jit? jit = null, long? affinity = null) => new EntryInfo { Job = new JobInfo { From b31a965f9111ac0556833163037d5183e5831237 Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Sat, 29 Aug 2026 02:30:17 +0500 Subject: [PATCH 2/4] do not run the wide mask test on a 32 bit runtime new IntPtr(1L << 40) throws OverflowException where IntPtr is four bytes, so the test i added would fail on a 32 bit run rather than prove anything. added a Platform64BitOnly requirement so it skips there, using the same FactEnvSpecific mechanism the other platform bound tests already use also added a test for the top bit. the 64th cpu is the last one FixAffinity handles without cpu groups and its mask only fits in a signed long as the negative value, so this pins down that it still reaches the right IntPtr --- tests/BenchmarkDotNet.Tests/ConfigParserTests.cs | 14 +++++++++++++- .../Shared/XUnit/EnvRequirement.cs | 1 + .../Shared/XUnit/EnvRequirementChecker.cs | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index 7147cb77db..ccb3097bc1 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -505,7 +505,7 @@ public void UserCanSpecifyAffinity() Assert.Equal(new IntPtr(affinity), config.GetJobs().Single().Environment.Affinity); } - [Fact] + [FactEnvSpecific("A mask with a bit above 32 does not fit in IntPtr on a 32 bit runtime", EnvRequirement.Platform64BitOnly)] public void UserCanSpecifyAffinityBeyondThirtyTwoProcessors() { const long affinity = 1L << 40; @@ -515,6 +515,18 @@ public void UserCanSpecifyAffinityBeyondThirtyTwoProcessors() Assert.Equal(new IntPtr(affinity), config.GetJobs().Single().Environment.Affinity); } + [FactEnvSpecific("A mask with the top bit set does not fit in IntPtr on a 32 bit runtime", EnvRequirement.Platform64BitOnly)] + public void UserCanSpecifyAffinityForTheSixtyFourthProcessor() + { + // the top bit is the 64th cpu, which is the last one FixAffinity supports without + // cpu groups. as a mask it reads 0x8000000000000000, which only fits in a signed + // long as the negative value, so this is the spelling that reaches that processor + var config = ConfigParser.Parse(["--affinity", long.MinValue.ToString()], new OutputLogger(Output)).config; + + Assert.NotNull(config); + Assert.Equal(unchecked((IntPtr)long.MinValue), config.GetJobs().Single().Environment.Affinity); + } + [Fact] public void UserCanSpecifyBuildTimeout() { diff --git a/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs b/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs index 1e44f59fb0..d193ba41db 100644 --- a/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs +++ b/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs @@ -10,6 +10,7 @@ public enum EnvRequirement FullFrameworkOnly, NonFullFramework, DotNetCoreOnly, + Platform64BitOnly, NeedsPrivilegedProcess, NonGitHubDraftPR, } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirementChecker.cs b/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirementChecker.cs index 3c7ecd931f..82cf6de05d 100644 --- a/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirementChecker.cs +++ b/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirementChecker.cs @@ -20,6 +20,7 @@ public static class EnvRequirementChecker EnvRequirement.FullFrameworkOnly => BdnRuntimeInformation.IsFullFramework ? null : "Full .NET Framework-only test", EnvRequirement.NonFullFramework => !BdnRuntimeInformation.IsFullFramework ? null : "Non-Full .NET Framework test", EnvRequirement.DotNetCoreOnly => BdnRuntimeInformation.IsNetCore ? null : ".NET/.NET Core-only test", + EnvRequirement.Platform64BitOnly => BdnRuntimeInformation.Is64BitPlatform() ? null : "64 bit platform-only test", EnvRequirement.NeedsPrivilegedProcess => IsPrivilegedProcess() ? null : "Needs authorization to perform security-relevant functions", EnvRequirement.NonGitHubDraftPR => !IsGitHubDraftPR() ? null : "GitHub draft PR", _ => throw new ArgumentOutOfRangeException(nameof(requirement), requirement, "Unknown value") From bd349a7af323edf32c642fa7b38c526c7a5f9605 Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Sat, 29 Aug 2026 03:00:48 +0500 Subject: [PATCH 3/4] make the affinity option unsigned a mask is not a signed number and the 64th cpu needed to be written as -9223372036854775808 to reach it. now it is written the way the mask reads the perfonar model stays long. perfolizers LightJsonSerializer throws Unsupported type: System.UInt64 so making that half unsigned breaks PerfonarTableTest at runtime. the value is only carried there so a signed long holds the same bits and nothing is lost conversion to IntPtr goes through new IntPtr(unchecked((long)value)) which is the same shape FixAffinity already uses --- .../ConsoleArguments/CommandLineOptions.cs | 2 +- .../ConsoleArguments/ConfigParser.cs | 2 +- .../BenchmarkDotNet.Tests/ConfigParserTests.cs | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index 17a4969e8e..e27cf54364 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -64,7 +64,7 @@ public bool UseDisassemblyDiagnoser public OutlierMode Outliers { get; set; } [Option("affinity", Required = false, HelpText = "Affinity mask to set for the benchmark process")] - public long? Affinity { get; set; } + public ulong? Affinity { get; set; } [Option("allStats", Required = false, Default = false, HelpText = "Displays all statistics (min, max & more)")] public bool DisplayAllStatistics { get; set; } diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 3c15744439..0ea5683a2e 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -470,7 +470,7 @@ private static Job GetBaseJob(CommandLineOptions options, IConfig? globalConfig) baseJob = baseJob.WithOutlierMode(options.Outliers); if (options.Affinity.HasValue) - baseJob = baseJob.WithAffinity((IntPtr)options.Affinity.Value); + baseJob = baseJob.WithAffinity(new IntPtr(unchecked((long)options.Affinity.Value))); if (options.LaunchCount.HasValue) baseJob = baseJob.WithLaunchCount(options.LaunchCount.Value); diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index ccb3097bc1..1987e39e34 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -498,33 +498,33 @@ public void PackagesPathParsedCorrectly() [Fact] public void UserCanSpecifyAffinity() { - const long affinity = 0b1010; + const ulong affinity = 0b1010; var config = ConfigParser.Parse(["--affinity", affinity.ToString()], new OutputLogger(Output)).config; Assert.NotNull(config); - Assert.Equal(new IntPtr(affinity), config.GetJobs().Single().Environment.Affinity); + Assert.Equal(new IntPtr((long)affinity), config.GetJobs().Single().Environment.Affinity); } [FactEnvSpecific("A mask with a bit above 32 does not fit in IntPtr on a 32 bit runtime", EnvRequirement.Platform64BitOnly)] public void UserCanSpecifyAffinityBeyondThirtyTwoProcessors() { - const long affinity = 1L << 40; + const ulong affinity = 1UL << 40; var config = ConfigParser.Parse(["--affinity", affinity.ToString()], new OutputLogger(Output)).config; Assert.NotNull(config); - Assert.Equal(new IntPtr(affinity), config.GetJobs().Single().Environment.Affinity); + Assert.Equal(new IntPtr((long)affinity), config.GetJobs().Single().Environment.Affinity); } [FactEnvSpecific("A mask with the top bit set does not fit in IntPtr on a 32 bit runtime", EnvRequirement.Platform64BitOnly)] public void UserCanSpecifyAffinityForTheSixtyFourthProcessor() { - // the top bit is the 64th cpu, which is the last one FixAffinity supports without - // cpu groups. as a mask it reads 0x8000000000000000, which only fits in a signed - // long as the negative value, so this is the spelling that reaches that processor - var config = ConfigParser.Parse(["--affinity", long.MinValue.ToString()], new OutputLogger(Output)).config; + // the top bit is the 64th cpu, the last one FixAffinity supports without cpu groups. + // as an unsigned option it is written the way the mask reads + const ulong affinity = 1UL << 63; + var config = ConfigParser.Parse(["--affinity", affinity.ToString()], new OutputLogger(Output)).config; Assert.NotNull(config); - Assert.Equal(unchecked((IntPtr)long.MinValue), config.GetJobs().Single().Environment.Affinity); + Assert.Equal(new IntPtr(unchecked((long)affinity)), config.GetJobs().Single().Environment.Affinity); } [Fact] From fb970c9ca03d0bfd64a64be2dbfdc778a86e955d Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Sun, 30 Aug 2026 21:53:58 +0500 Subject: [PATCH 4/4] reject an affinity mask a 32 bit process can not hold the option is now unsigned so a value above int.MaxValue can reach new IntPtr(long) which throws on a 32 bit process instead of truncating. that made ConfigParser.Parse fail with an unhandled OverflowException rather than a normal option error. the conversion now goes through TryConvertAffinity which takes the pointer size. on 8 bytes it keeps the full 64 bit mask as before. on 4 bytes it takes the low 32 bits so a full 32 processor mask still works and it reports failure for anything wider so Validate can print an error. --- .../ConsoleArguments/ConfigParser.cs | 23 +++++++++++++-- .../ConfigParserTests.cs | 29 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 0ea5683a2e..2e2521d445 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -380,9 +380,28 @@ private static bool Validate(CommandLineOptions options, ILogger logger) return false; } + if (options.Affinity.HasValue && !TryConvertAffinity(options.Affinity.Value, IntPtr.Size, out _)) + { + logger.WriteLineError($"The provided affinity mask 0x{options.Affinity.Value:X} does not fit into the {IntPtr.Size * 8} bit process that hosts the benchmarks. Use a mask of at most 32 bits or run in a 64 bit process."); + return false; + } + return true; } + // a 32 bit process only reaches the first 32 processors and new IntPtr(long) throws there instead of truncating + internal static bool TryConvertAffinity(ulong mask, int pointerSize, out IntPtr affinity) + { + if (pointerSize >= 8) + { + affinity = new IntPtr(unchecked((long)mask)); + return true; + } + + affinity = new IntPtr(unchecked((int)mask)); + return mask <= uint.MaxValue; + } + private static IConfig CreateConfig(CommandLineOptions options, IConfig? globalConfig, string[] args) { var config = new ManualConfig(); @@ -469,8 +488,8 @@ private static Job GetBaseJob(CommandLineOptions options, IConfig? globalConfig) if (baseJob != Job.Dry && options.Outliers != OutlierMode.RemoveUpper) baseJob = baseJob.WithOutlierMode(options.Outliers); - if (options.Affinity.HasValue) - baseJob = baseJob.WithAffinity(new IntPtr(unchecked((long)options.Affinity.Value))); + if (options.Affinity.HasValue && TryConvertAffinity(options.Affinity.Value, IntPtr.Size, out var affinity)) + baseJob = baseJob.WithAffinity(affinity); if (options.LaunchCount.HasValue) baseJob = baseJob.WithLaunchCount(options.LaunchCount.Value); diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index 1987e39e34..7869fb30a9 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -527,6 +527,35 @@ public void UserCanSpecifyAffinityForTheSixtyFourthProcessor() Assert.Equal(new IntPtr(unchecked((long)affinity)), config.GetJobs().Single().Environment.Affinity); } + [Theory] + [InlineData(0b1010UL)] + [InlineData(1UL << 31)] + [InlineData(uint.MaxValue)] + public void AffinityThatFitsThirtyTwoBitsIsAcceptedByAThirtyTwoBitProcess(ulong affinity) + { + Assert.True(ConfigParser.TryConvertAffinity(affinity, pointerSize: 4, out var converted)); + Assert.Equal(new IntPtr(unchecked((int)affinity)), converted); + } + + [Theory] + [InlineData(1UL << 32)] + [InlineData(1UL << 40)] + [InlineData(1UL << 63)] + public void AffinityWiderThanThirtyTwoBitsIsRejectedByAThirtyTwoBitProcess(ulong affinity) + { + Assert.False(ConfigParser.TryConvertAffinity(affinity, pointerSize: 4, out _)); + } + + [Theory] + [InlineData(0b1010UL)] + [InlineData(1UL << 40)] + [InlineData(1UL << 63)] + public void AffinityOfAnyWidthIsAcceptedByASixtyFourBitProcess(ulong affinity) + { + Assert.True(ConfigParser.TryConvertAffinity(affinity, pointerSize: 8, out var converted)); + Assert.Equal(new IntPtr(unchecked((long)affinity)), converted); + } + [Fact] public void UserCanSpecifyBuildTimeout() {