diff --git a/docs/articles/samples/IntroComparableComplexParam.md b/docs/articles/samples/IntroComparableComplexParam.md new file mode 100644 index 0000000000..16319bab9c --- /dev/null +++ b/docs/articles/samples/IntroComparableComplexParam.md @@ -0,0 +1,20 @@ +--- +uid: BenchmarkDotNet.Samples.IntroComparableComplexParam +--- + +## Sample: IntroComparableComplexParam + +You can implement `IComparable` (the non generic version) on your complex parameter class if you want custom ordering behavior for your parameter. + +One use case for this is having a parameter class that overrides `ToString()`, but also providing a custom ordering behavior that isn't the alphabetical order of the result of `ToString()`. + +### Source code + +[!code-csharp[IntroComparableComplexParam.cs](../../../samples/BenchmarkDotNet.Samples/IntroComparableComplexParam.cs)] + +### Links + +* @docs.parameterization +* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroComparableComplexParam + +--- \ No newline at end of file diff --git a/docs/articles/samples/toc.yml b/docs/articles/samples/toc.yml index 9d30bb4c64..098e457dd8 100644 --- a/docs/articles/samples/toc.yml +++ b/docs/articles/samples/toc.yml @@ -14,6 +14,8 @@ href: IntroCategoryBaseline.md - name: IntroColdStart href: IntroColdStart.md +- name: IntroComparableComplexParam + href: IntroComparableComplexParam.md - name: IntroConfigSource href: IntroConfigSource.md - name: IntroConfigUnion diff --git a/samples/BenchmarkDotNet.Samples/IntroComparableComplexParam.cs b/samples/BenchmarkDotNet.Samples/IntroComparableComplexParam.cs new file mode 100644 index 0000000000..4bb5cffb22 --- /dev/null +++ b/samples/BenchmarkDotNet.Samples/IntroComparableComplexParam.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; + +namespace BenchmarkDotNet.Samples +{ + public class IntroComparableComplexParam + { + [ParamsSource(nameof(ValuesForA))] + public ComplexParam A { get; set; } + + public IEnumerable ValuesForA => new[] { new ComplexParam(1, "First"), new ComplexParam(2, "Second") }; + + [Benchmark] + public object Benchmark() => A; + + // Only non generic IComparable is required to provide custom order behavior, but implementing IComparable<> too is customary. + public class ComplexParam : IComparable, IComparable + { + public ComplexParam(int value, string name) + { + Value = value; + Name = name; + } + + public int Value { get; set; } + + public string Name { get; set; } + + public override string ToString() => Name; + + public int CompareTo(ComplexParam other) => other == null ? 1 : Value.CompareTo(other.Value); + + public int CompareTo(object obj) => obj is ComplexParam other ? CompareTo(other) : throw new ArgumentException(); + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Parameters/ParameterComparer.cs b/src/BenchmarkDotNet/Parameters/ParameterComparer.cs index e5ae86d57b..a1363de073 100644 --- a/src/BenchmarkDotNet/Parameters/ParameterComparer.cs +++ b/src/BenchmarkDotNet/Parameters/ParameterComparer.cs @@ -7,16 +7,6 @@ internal class ParameterComparer : IComparer { public static readonly ParameterComparer Instance = new ParameterComparer(); - // We will only worry about common, basic types, i.e. int, long, double, etc - // (e.g. you can't write [Params(10.0m, 20.0m, 100.0m, 200.0m)], the compiler won't let you!) - private static readonly Comparer PrimitiveComparer = new Comparer(). - Add((string x, string y) => string.CompareOrdinal(x, y)). - Add((int x, int y) => x.CompareTo(y)). - Add((long x, long y) => x.CompareTo(y)). - Add((short x, short y) => x.CompareTo(y)). - Add((float x, float y) => x.CompareTo(y)). - Add((double x, double y) => x.CompareTo(y)); - public int Compare(ParameterInstances x, ParameterInstances y) { if (x == null && y == null) return 0; @@ -24,35 +14,25 @@ public int Compare(ParameterInstances x, ParameterInstances y) if (x == null) return -1; for (int i = 0; i < Math.Min(x.Count, y.Count); i++) { - int compareTo = PrimitiveComparer.CompareTo(x[i]?.Value, y[i]?.Value); + var compareTo = CompareValues(x[i]?.Value, y[i]?.Value); if (compareTo != 0) return compareTo; } return string.CompareOrdinal(x.DisplayInfo, y.DisplayInfo); } - private class Comparer + private int CompareValues(object x, object y) { - private readonly Dictionary> comparers = - new Dictionary>(); - - public Comparer Add(Func compareFunc) - { - comparers.Add(typeof(T), (x, y) => compareFunc((T)x, (T)y)); - return this; - } - - public int CompareTo(object x, object y) + // Detect IComparable implementations. + // This works for all primitive types in addition to user types that implement IComparable. + if (x != null && y != null && x.GetType() == y.GetType() && + x is IComparable xComparable) { - return x != null && y != null && x.GetType() == y.GetType() && comparers.TryGetValue(GetComparisonType(x), out var comparer) - ? comparer(x, y) - : string.CompareOrdinal(x?.ToString(), y?.ToString()); + return xComparable.CompareTo(y); } - private static Type GetComparisonType(object x) => - x.GetType().IsEnum - ? x.GetType().GetEnumUnderlyingType() - : x.GetType(); + // Anything else. + return string.CompareOrdinal(x?.ToString(), y?.ToString()); } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs b/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs index f67e6b3d30..0f01045b5c 100644 --- a/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs +++ b/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs @@ -122,5 +122,82 @@ public void AlphaNumericComparisionTest() Assert.Equal(1000, sortedData[2].Items[0].Value); Assert.Equal(2000, sortedData[3].Items[0].Value); } + + [Fact] + public void IComparableComparisionTest() + { + var comparer = ParameterComparer.Instance; + + var originalData = new[] + { + new ParameterInstances(new[] + { + new ParameterInstance(sharedDefinition, new ComplexParameter(1, "first"), null) + }), + new ParameterInstances(new[] + { + new ParameterInstance(sharedDefinition, new ComplexParameter(3, "third"), null) + }), + new ParameterInstances(new[] + { + new ParameterInstance(sharedDefinition, new ComplexParameter(2, "second"), null) + }), + new ParameterInstances(new[] + { + new ParameterInstance(sharedDefinition, new ComplexParameter(4, "fourth"), null) + }) + }; + + var sortedData = originalData.OrderBy(d => d, comparer).ToArray(); + + // Check that we sort by numeric value, not string order!! + Assert.Equal(1, ((ComplexParameter)sortedData[0].Items[0].Value).Value); + Assert.Equal(2, ((ComplexParameter)sortedData[1].Items[0].Value).Value); + Assert.Equal(3, ((ComplexParameter)sortedData[2].Items[0].Value).Value); + Assert.Equal(4, ((ComplexParameter)sortedData[3].Items[0].Value).Value); + } + + private class ComplexParameter : IComparable, IComparable + { + public ComplexParameter(int value, string name) + { + Value = value; + Name = name; + } + + public int Value { get; } + + public string Name { get; } + + public override string ToString() + { + return Name; + } + + public int CompareTo(ComplexParameter other) + { + if (other == null) + { + return 1; + } + + return Value.CompareTo(other.Value); + } + + public int CompareTo(object obj) + { + if (obj == null) + { + return 1; + } + + if (obj is not ComplexParameter other) + { + throw new ArgumentException(); + } + + return CompareTo(other); + } + } } } \ No newline at end of file