Skip to content

Commit 415ac91

Browse files
User0332timcassell
andauthored
Compare parameters by object value rather than string representation (Related to #2838) (#2887)
* Compare and group benchmark parameters by object value rather than string representation * Add test for grouping benchmark parameters which are collections * Remove whitespace from params logical group key * Update Verify files to account for logical group key changes * Implement param equality comparison directly * Ensure that params enumerable comparisons return false when collection lengths are different * Remove false comment from ParameterEqualityComparer.cs * Add parameter comparison fallback if collection elements are not comparable * Optimize parameter comparison for enumerables * Implement feedback for parameter comparison * Implement PR feedback. Added a TODO note for performance. --------- Co-authored-by: Tim Cassell <cassell.timothy@gmail.com>
1 parent c1dc751 commit 415ac91

17 files changed

+859
-193
lines changed

src/BenchmarkDotNet/Order/DefaultOrderer.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ public string GetHighlightGroupKey(BenchmarkCase benchmarkCase)
8989

9090
public string GetLogicalGroupKey(ImmutableArray<BenchmarkCase> allBenchmarksCases, BenchmarkCase benchmarkCase)
9191
{
92+
// TODO: GetLogicalGroupKey is called for every benchmarkCase, so as the number of cases grows, this can get very expensive to recompute for each call.
93+
// We should somehow amortize the cost by computing it only once per summary.
94+
var paramSets = allBenchmarksCases.Select(benchmarkCase => benchmarkCase.Parameters).Distinct(ParameterEqualityComparer.Instance).ToArray();
95+
9296
var explicitRules = benchmarkCase.Config.GetLogicalGroupRules().ToList();
9397
var implicitRules = new List<BenchmarkLogicalGroupRule>();
9498
bool hasJobBaselines = allBenchmarksCases.Any(b => b.Job.Meta.Baseline);
@@ -125,7 +129,7 @@ public string GetLogicalGroupKey(ImmutableArray<BenchmarkCase> allBenchmarksCase
125129
keys.Add(benchmarkCase.Job.DisplayInfo);
126130
break;
127131
case BenchmarkLogicalGroupRule.ByParams:
128-
keys.Add(benchmarkCase.Parameters.ValueInfo);
132+
keys.Add($"DistinctParamSet{Array.FindIndex(paramSets, (paramSet) => ParameterEqualityComparer.Instance.Equals(paramSet, benchmarkCase.Parameters))}");
129133
break;
130134
case BenchmarkLogicalGroupRule.ByCategory:
131135
keys.Add(string.Join(",", benchmarkCase.Descriptor.Categories));
Lines changed: 255 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,287 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
4+
using System.Reflection;
5+
using BenchmarkDotNet.Portability;
6+
7+
#if NETSTANDARD2_0
8+
using System.Collections.Concurrent;
9+
using System.Linq;
10+
#endif
311

412
namespace BenchmarkDotNet.Parameters
513
{
614
internal class ParameterComparer : IComparer<ParameterInstances>
715
{
16+
#if NETSTANDARD2_0
17+
private static readonly ConcurrentDictionary<Type, MemberInfo[]> s_tupleMembersCache = new();
18+
19+
private static MemberInfo[] GetTupleMembers(Type type)
20+
=> s_tupleMembersCache.GetOrAdd(type, t =>
21+
{
22+
var members = type.FullName.StartsWith("System.Tuple`")
23+
? type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Cast<MemberInfo>()
24+
: type.GetFields(BindingFlags.Public | BindingFlags.Instance).Cast<MemberInfo>();
25+
return members
26+
.Where(p => p.Name.StartsWith("Item"))
27+
.OrderBy(p => p.Name)
28+
.ToArray();
29+
});
30+
#endif
31+
832
public static readonly ParameterComparer Instance = new ParameterComparer();
933

1034
public int Compare(ParameterInstances x, ParameterInstances y)
1135
{
1236
if (x == null && y == null) return 0;
13-
if (x != null && y == null) return 1;
1437
if (x == null) return -1;
38+
if (y == null) return 1;
39+
1540
for (int i = 0; i < Math.Min(x.Count, y.Count); i++)
1641
{
17-
var compareTo = CompareValues(x[i]?.Value, y[i]?.Value);
18-
if (compareTo != 0)
19-
return compareTo;
42+
var comparison = CompareValues(x[i]?.Value, y[i]?.Value);
43+
if (comparison != 0)
44+
{
45+
return comparison;
46+
}
2047
}
48+
2149
return string.CompareOrdinal(x.DisplayInfo, y.DisplayInfo);
2250
}
2351

24-
private int CompareValues(object x, object y)
52+
private static int CompareValues<T1, T2>(T1 x, T2 y)
2553
{
26-
// Detect IComparable implementations.
27-
// This works for all primitive types in addition to user types that implement IComparable.
28-
if (x != null && y != null && x.GetType() == y.GetType() &&
29-
x is IComparable xComparable)
54+
if (x == null || y == null || x.GetType() != y.GetType())
55+
{
56+
return string.CompareOrdinal(x?.ToString(), y?.ToString());
57+
}
58+
59+
if (x is IStructuralComparable xStructuralComparable)
3060
{
3161
try
3262
{
33-
return xComparable.CompareTo(y);
63+
return StructuralComparisons.StructuralComparer.Compare(x, y);
3464
}
35-
// Some types, such as Tuple and ValueTuple, have a fallible CompareTo implementation which can throw if the inner items don't implement IComparable.
36-
// See: https://github.com/dotnet/BenchmarkDotNet/issues/2346
37-
// For now, catch and ignore the exception, and fallback to string comparison below.
38-
catch (ArgumentException ex) when (ex.Message.Contains("At least one object must implement IComparable."))
65+
// https://github.com/dotnet/BenchmarkDotNet/issues/2346
66+
// https://github.com/dotnet/runtime/issues/66472
67+
// Unfortunately we can't rely on checking the exception message because it may change per current culture.
68+
catch (ArgumentException)
3969
{
70+
if (TryFallbackStructuralCompareTo(x, y, out int comparison))
71+
{
72+
return comparison;
73+
}
74+
// A complex user type did not handle a multi-dimensional array or tuple, just re-throw.
75+
throw;
4076
}
4177
}
4278

43-
// Anything else.
79+
if (x is IComparable xComparable)
80+
{
81+
// Tuples are already handled by IStructuralComparable case, if this throws, it's the user's own fault.
82+
return xComparable.CompareTo(y);
83+
}
84+
85+
if (x is IEnumerable xEnumerable) // General collection comparison support
86+
{
87+
return CompareEnumerables(xEnumerable, (IEnumerable) y);
88+
}
89+
90+
// Anything else to differentiate between objects.
4491
return string.CompareOrdinal(x?.ToString(), y?.ToString());
4592
}
93+
94+
private static bool TryFallbackStructuralCompareTo(object x, object y, out int comparison)
95+
{
96+
// Check for multi-dimensional array and ITuple and re-try for each element recursively.
97+
if (x is Array xArr)
98+
{
99+
Array yArr = (Array) y;
100+
if (xArr.Rank != yArr.Rank)
101+
{
102+
comparison = xArr.Rank.CompareTo(yArr.Rank);
103+
return true;
104+
}
105+
106+
for (int dim = 0; dim < xArr.Rank; dim++)
107+
{
108+
if (xArr.GetLength(dim) != yArr.GetLength(dim))
109+
{
110+
comparison = xArr.GetLength(dim).CompareTo(yArr.GetLength(dim));
111+
return true;
112+
}
113+
}
114+
115+
// Common 2D and 3D arrays are specialized to avoid expensive boxing where possible.
116+
if (!RuntimeInformation.IsAot && xArr.Rank is 2 or 3)
117+
{
118+
string methodName = xArr.Rank == 2
119+
? nameof(CompareTwoDArray)
120+
: nameof(CompareThreeDArray);
121+
comparison = (int) typeof(ParameterComparer)
122+
.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static)
123+
.MakeGenericMethod(xArr.GetType().GetElementType(), yArr.GetType().GetElementType())
124+
.Invoke(null, [xArr, yArr]);
125+
return true;
126+
}
127+
128+
// 1D arrays will only hit this code path if a nested type is a multi-dimensional array.
129+
// 4D and larger fall back to enumerable.
130+
comparison = CompareEnumerables(xArr, yArr);
131+
return true;
132+
}
133+
134+
#if NETSTANDARD2_0
135+
// ITuple does not exist in netstandard2.0, so we have to use reflection. ITuple does exist in net471 and newer, but the System.ValueTuple nuget package does not implement it.
136+
string typeName = x.GetType().FullName;
137+
if (typeName.StartsWith("System.Tuple`"))
138+
{
139+
comparison = CompareTuples(x, y);
140+
return true;
141+
}
142+
else if (typeName.StartsWith("System.ValueTuple`"))
143+
{
144+
comparison = CompareValueTuples(x, y);
145+
return true;
146+
}
147+
#else
148+
if (x is System.Runtime.CompilerServices.ITuple xTuple)
149+
{
150+
comparison = CompareTuples(xTuple, (System.Runtime.CompilerServices.ITuple) y);
151+
return true;
152+
}
153+
#endif
154+
155+
if (x is IEnumerable xEnumerable) // General collection equality support
156+
{
157+
comparison = CompareEnumerables(xEnumerable, (IEnumerable) y);
158+
return true;
159+
}
160+
161+
comparison = 0;
162+
return false;
163+
}
164+
165+
private static int CompareEnumerables(IEnumerable x, IEnumerable y)
166+
{
167+
var xEnumerator = x.GetEnumerator();
168+
try
169+
{
170+
var yEnumerator = y.GetEnumerator();
171+
try
172+
{
173+
while (xEnumerator.MoveNext())
174+
{
175+
if (!yEnumerator.MoveNext())
176+
{
177+
return -1;
178+
}
179+
int comparison = CompareValues(xEnumerator.Current, yEnumerator.Current);
180+
if (comparison != 0)
181+
{
182+
return comparison;
183+
}
184+
}
185+
return yEnumerator.MoveNext() ? 1 : 0;
186+
}
187+
finally
188+
{
189+
if (yEnumerator is IDisposable disposable)
190+
{
191+
disposable.Dispose();
192+
}
193+
}
194+
}
195+
finally
196+
{
197+
if (xEnumerator is IDisposable disposable)
198+
{
199+
disposable.Dispose();
200+
}
201+
}
202+
}
203+
204+
private static int CompareTwoDArray<T1, T2>(T1[,] arrOne, T2[,] arrTwo)
205+
{
206+
// Assumes that arrOne & arrTwo are the same length and width.
207+
for (int i = 0; i < arrOne.GetLength(0); i++)
208+
{
209+
for (int j = 0; j < arrOne.GetLength(1); j++)
210+
{
211+
var comparison = CompareValues(arrOne[i, j], arrTwo[i, j]);
212+
if (comparison != 0)
213+
{
214+
return comparison;
215+
}
216+
}
217+
}
218+
219+
return 0;
220+
}
221+
222+
private static int CompareThreeDArray<T1, T2>(T1[,,] arrOne, T2[,,] arrTwo)
223+
{
224+
// Assumes that arrOne & arrTwo are the same length, width, and height.
225+
for (int i = 0; i < arrOne.GetLength(0); i++)
226+
{
227+
for (int j = 0; j < arrOne.GetLength(1); j++)
228+
{
229+
for (int k = 0; k <arrOne.GetLength(2); k++)
230+
{
231+
var comparison = CompareValues(arrOne[i, j, k], arrTwo[i, j, k]);
232+
if (comparison != 0)
233+
{
234+
return comparison;
235+
}
236+
}
237+
}
238+
}
239+
240+
return 0;
241+
}
242+
243+
#if NETSTANDARD2_0
244+
private static int CompareTuples(object x, object y)
245+
{
246+
foreach (PropertyInfo property in GetTupleMembers(x.GetType()))
247+
{
248+
var comparison = CompareValues(property.GetValue(x), property.GetValue(y));
249+
if (comparison != 0)
250+
{
251+
return comparison;
252+
}
253+
}
254+
255+
return 0;
256+
}
257+
258+
private static int CompareValueTuples(object x, object y)
259+
{
260+
foreach (FieldInfo field in GetTupleMembers(x.GetType()))
261+
{
262+
var comparison = CompareValues(field.GetValue(x), field.GetValue(y));
263+
if (comparison != 0)
264+
{
265+
return comparison;
266+
}
267+
}
268+
269+
return 0;
270+
}
271+
#else
272+
private static int CompareTuples(System.Runtime.CompilerServices.ITuple x, System.Runtime.CompilerServices.ITuple y)
273+
{
274+
for (int i = 0; i < x.Length; i++)
275+
{
276+
var comparison = CompareValues(x[i], y[i]);
277+
if (comparison != 0)
278+
{
279+
return comparison;
280+
}
281+
}
282+
283+
return 0;
284+
}
285+
#endif
46286
}
47287
}

0 commit comments

Comments
 (0)