From 80142468bf3a15662aac60441351407ca25e621a Mon Sep 17 00:00:00 2001 From: Carl Furtado Date: Sun, 30 Nov 2025 13:51:44 -0500 Subject: [PATCH 1/2] Allow builtin support to differentiate array arguments by value comparison --- src/BenchmarkDotNet/Code/ArrayParam.cs | 19 ++++++++++++++ src/BenchmarkDotNet/Code/EnumParam.cs | 2 ++ src/BenchmarkDotNet/Code/IParam.cs | 5 ++++ .../Parameters/ParameterInstance.cs | 25 +++++++++++++++++++ .../Parameters/ParameterInstances.cs | 2 +- .../Parameters/SmartParamBuilder.cs | 4 +++ 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Code/ArrayParam.cs b/src/BenchmarkDotNet/Code/ArrayParam.cs index 536f126f21..f7f0adceb2 100644 --- a/src/BenchmarkDotNet/Code/ArrayParam.cs +++ b/src/BenchmarkDotNet/Code/ArrayParam.cs @@ -11,6 +11,23 @@ internal static class ArrayParam { public static string GetDisplayString(Array array) => $"{array.GetType().GetElementType()?.GetDisplayName()}[{array.Length}]"; + + public static string GetValueString(Array array) + => $"{GetDisplayString(array)} (rank {array.Rank}, hash {GetArrayValueHash(array)})"; + + private static int GetArrayValueHash(Array array) + { + var arrFlat = array.Cast(); + + int hash = 0; + + foreach (var elem in arrFlat) + { + hash = HashCode.Combine(hash, elem); + } + + return hash; + } } public class ArrayParam : IParam @@ -28,6 +45,8 @@ private ArrayParam(T[] array, Func? toSourceCode = null) public string DisplayText => ArrayParam.GetDisplayString(array); + public string ValueText => ArrayParam.GetValueString(array); + public string ToSourceCode() => $"new {typeof(T).GetCorrectCSharpTypeName()}[] {{ {string.Join(", ", array.Select(item => toSourceCode?.Invoke(item) ?? SourceCodeHelper.ToSourceCode(item)))} }}"; diff --git a/src/BenchmarkDotNet/Code/EnumParam.cs b/src/BenchmarkDotNet/Code/EnumParam.cs index 5c0ecf6afd..da87563c43 100644 --- a/src/BenchmarkDotNet/Code/EnumParam.cs +++ b/src/BenchmarkDotNet/Code/EnumParam.cs @@ -21,6 +21,8 @@ private EnumParam(object value, Type type) public string DisplayText => $"{Enum.ToObject(type, Value)}"; + public string ValueText => DisplayText; + public string ToSourceCode() => $"({type.GetCorrectCSharpTypeName()})({ToInvariantCultureString()})"; diff --git a/src/BenchmarkDotNet/Code/IParam.cs b/src/BenchmarkDotNet/Code/IParam.cs index 7ac5845868..7becefde00 100644 --- a/src/BenchmarkDotNet/Code/IParam.cs +++ b/src/BenchmarkDotNet/Code/IParam.cs @@ -13,6 +13,11 @@ public interface IParam /// string DisplayText { get; } + /// + /// used to group the value + /// + string ValueText { get; } + /// /// this source code is used to create parameter for benchmark /// in C# source code file diff --git a/src/BenchmarkDotNet/Parameters/ParameterInstance.cs b/src/BenchmarkDotNet/Parameters/ParameterInstance.cs index 276ca6f669..44536ff972 100644 --- a/src/BenchmarkDotNet/Parameters/ParameterInstance.cs +++ b/src/BenchmarkDotNet/Parameters/ParameterInstance.cs @@ -62,6 +62,31 @@ public string ToDisplayText(SummaryStyle summary) public string ToDisplayText() => ToDisplayText(CultureInfo.CurrentCulture, maxParameterColumnWidthFromConfig); + private string ToValueText(CultureInfo cultureInfo, int maxParameterColumnWidth) + { + switch (value) + { + case null: + return NullParameterTextRepresentation; + case IParam parameter: + return Trim(parameter.ValueText, maxParameterColumnWidth).EscapeSpecialCharacters(false); + case IFormattable formattable: + return Trim(formattable.ToString(null, cultureInfo), maxParameterColumnWidth).EscapeSpecialCharacters(false); + // no trimming for types! + case Type type: + return type.IsNullable() ? $"{Nullable.GetUnderlyingType(type).GetDisplayName()}?" : type.GetDisplayName(); + default: + return Trim(value.ToString(), maxParameterColumnWidth).EscapeSpecialCharacters(false); + } + } + + public string ToValueText(SummaryStyle summary) + { + return summary != null ? ToValueText(summary.CultureInfo, summary.MaxParameterColumnWidth) : ToValueText(); + } + + public string ToValueText() => ToValueText(CultureInfo.CurrentCulture, maxParameterColumnWidthFromConfig); + public override string ToString() => ToDisplayText(); private static string Trim(string value, int maxDisplayTextInnerLength) diff --git a/src/BenchmarkDotNet/Parameters/ParameterInstances.cs b/src/BenchmarkDotNet/Parameters/ParameterInstances.cs index 1f83b59310..aaaf4b7d0a 100644 --- a/src/BenchmarkDotNet/Parameters/ParameterInstances.cs +++ b/src/BenchmarkDotNet/Parameters/ParameterInstances.cs @@ -31,7 +31,7 @@ public void Dispose() public string DisplayInfo => Items.Any() ? "[" + string.Join(", ", Items.Select(p => $"{p.Name}={p.ToDisplayText()}")) + "]" : ""; - public string ValueInfo => Items.Any() ? "[" + string.Join(", ", Items.Select(p => $"{p.Name}={p.Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation}")) + "]" : ""; + public string ValueInfo => Items.Any() ? "[" + string.Join(", ", Items.Select(p => $"{p.Name}={p.ToValueText()}")) + "]" : ""; public string PrintInfo => printInfo ?? (printInfo = string.Join("&", Items.Select(p => $"{p.Name}={p.ToDisplayText()}"))); diff --git a/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs b/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs index adc9789ef8..abd1523aa9 100644 --- a/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs +++ b/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs @@ -91,6 +91,8 @@ public SmartArgument(ParameterDefinition[] parameterDefinitions, object value, M public string DisplayText => Value is Array array ? ArrayParam.GetDisplayString(array) : Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation; + public string ValueText => Value is Array array ? ArrayParam.GetValueString(array) : DisplayText; + public string ToSourceCode() { Type paramType = parameterDefinitions[argumentIndex].ParameterType; @@ -153,6 +155,8 @@ public SmartParameter(Type parameterType, MemberInfo source, object value, int i public string DisplayText => Value is Array array ? ArrayParam.GetDisplayString(array) : Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation; + public string ValueText => Value is Array array ? ArrayParam.GetValueString(array) : DisplayText; + public string ToSourceCode() { string cast = $"({parameterType.GetCorrectCSharpTypeName()})"; // it's an object so we need to cast it to the right type From 975a572264664b8ac62dd96eb9f0815aea091f5f Mon Sep 17 00:00:00 2001 From: Carl Furtado Date: Sun, 30 Nov 2025 13:52:02 -0500 Subject: [PATCH 2/2] Add test case for differentiating array arguments by value --- .../ArgumentsTests.cs | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs b/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs index 76edfe5d4e..a21bcdf007 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Tests.XUnit; using BenchmarkDotNet.Toolchains; @@ -311,9 +312,9 @@ public void Test(int[][] array) throw new ArgumentNullException(nameof(array)); for (int i = 0; i < 10; i++) - for (int j = 0; j < i; j++) - if (array[i][j] != i) - throw new ArgumentException("Invalid value"); + for (int j = 0; j < i; j++) + if (array[i][j] != i) + throw new ArgumentException("Invalid value"); } public IEnumerable CreateMatrix() @@ -543,6 +544,46 @@ public void AcceptsArrays(int[] even, int[] notEven) } } + [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void ArrayArgumentsCanBeGrouped(IToolchain toolchain) + { + var summary = CanExecute(toolchain, (config) => config.AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByParams)); // We must group by params to test array argument grouping + + // There should be two logical groups, one for the first argument and one for the second argument + // (thus there should be two pairs per descriptor, and each pair should be distinct because it belongs to a different group) + + var descriptorGroupPairs = summary.BenchmarksCases.Select(benchmarkCase => (benchmarkCase.Descriptor, summary.GetLogicalGroupKey(benchmarkCase))).GroupBy(benchmarkCase => benchmarkCase.Descriptor); + + Assert.True( + descriptorGroupPairs.All(group => group.Select(pair => pair.Item2).Distinct().Count() == 2) + ); + } + + public class ArrayOnDifferentMethods + { + public IEnumerable GetArrays() + { + yield return new int[] { 1, 2, 3 }; + yield return new int[] { 2, 3, 4 }; + } + + [Benchmark(Baseline = true)] + [ArgumentsSource(nameof(GetArrays))] + public void AcceptsArrays(int[] arr) + { + if (arr.Length != 3) + throw new ArgumentException("Incorrect length"); + } + + [Benchmark] + [ArgumentsSource(nameof(GetArrays))] + public void AcceptsArrays2(int[] arr) + { + if (arr.Length != 3) + throw new ArgumentException("Incorrect length"); + } + } + [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] public void VeryBigIntegersAreSupported(IToolchain toolchain) => CanExecute(toolchain); @@ -1026,6 +1067,16 @@ public Disposable(int id) } } - private void CanExecute(IToolchain toolchain) => CanExecute(CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain))); + private Reports.Summary CanExecute(IToolchain toolchain, Func furtherConfigure = null) + { + var config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + + if (furtherConfigure is not null) + { + config = furtherConfigure(config); + } + + return CanExecute(config); + } } } \ No newline at end of file