Last active
October 26, 2023 02:25
-
-
Save overing/57da080d07a79ade1a671633509c52c6 to your computer and use it in GitHub Desktop.
物件值比較器, 0 GC Alloc, 比 Json 比法快了最少 16x, 亦可調整成比較 field 而不是 property, 寫再多成員都只要呼叫 Comparer.IsEqual(x, y) 即可
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
using static System.Linq.Expressions.Expression; | |
public static class Comparer | |
{ | |
static readonly Dictionary<Type, Func<object, object, bool>> s_cache = new(); | |
public static bool IsEqual(object? x, object? y) | |
{ | |
if ((x == default && y != default) || (x != default && y == default)) | |
return false; | |
if (ReferenceEquals(x, y)) | |
return true; | |
var type = x!.GetType(); | |
if (type != y!.GetType()) | |
return false; | |
if (!s_cache.TryGetValue(type, out var func)) | |
{ | |
lock (s_cache) | |
{ | |
if (!s_cache.TryGetValue(type, out func)) | |
{ | |
func = Shareds.Build(type); | |
s_cache[type] = func; | |
} | |
} | |
} | |
return func(x, y); | |
} | |
} | |
public sealed class Comparer<T> : IEqualityComparer<T> | |
{ | |
static readonly Func<T?, T?, bool> s_equalFunc = Shareds.Build<T>(); | |
public static Comparer<T> Default { get; } = new(); | |
public bool IsEqual(T? x, T? y) => s_equalFunc(x, y); | |
bool IEqualityComparer<T>.Equals(T? x, T? y) => s_equalFunc(x, y); | |
int IEqualityComparer<T>.GetHashCode(T obj) => throw new NotSupportedException(); | |
} | |
static class Shareds | |
{ | |
static readonly MethodInfo s_sequenceEqualMethod = | |
typeof(Shareds).GetMethod(nameof(SequenceEqual), BindingFlags.Static | BindingFlags.NonPublic)!; | |
static readonly MethodInfo s_referenceEqualsMethod = typeof(object).GetMethod(nameof(object.ReferenceEquals))!; | |
static readonly MethodInfo s_getTypeMethod = typeof(object).GetMethod(nameof(object.GetType))!; | |
static readonly LabelTarget s_returnTarget = Label(typeof(bool)); | |
static readonly Expression s_returnTrue = Return(s_returnTarget, Constant(true)); | |
static readonly Expression s_returnFalse = Return(s_returnTarget, Constant(false)); | |
static readonly Expression s_returnLabel = Label(s_returnTarget, Constant(false)); | |
static bool SequenceEqual<TElement>(IEnumerable<TElement> x, IEnumerable<TElement> y, IEqualityComparer<TElement> c) | |
=> Enumerable.SequenceEqual(x, y, c); | |
static void GetComparer(Type type, out Expression comparer, out MethodInfo method) | |
{ | |
var comparerType = typeof(Comparer<>).MakeGenericType(type); | |
var comparerProperty = comparerType.GetProperty(nameof(Comparer<object>.Default))!; | |
comparer = Property(null, comparerProperty); | |
method = comparerType.GetMethod(nameof(Comparer<object>.IsEqual), new[] { type, type })!; | |
} | |
static Expression ComparePublicInstanceProperties(Type targetType, Expression xParam, Expression yParam) | |
=> targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public) | |
.Where(p => p.GetGetMethod() is not null) | |
.Aggregate(s_returnTrue, (previous, property) => | |
{ | |
GetComparer(property.PropertyType, out var comparer, out var method); | |
return IfThen(Call(comparer, method, Property(xParam, property), Property(yParam, property)), previous); | |
}); | |
static Expression CompareAllInstanceFields(Type targetType, Expression xParam, Expression yParam) | |
=> targetType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) | |
.Aggregate(s_returnTrue, (previous, field) => | |
{ | |
GetComparer(field.FieldType, out var comparer, out var method); | |
return IfThen(Call(comparer, method, Field(xParam, field), Field(yParam, field)), previous); | |
}); | |
public static Func<T?, T?, bool> Build<T>() | |
{ | |
if (typeof(IEquatable<T>).IsAssignableFrom(typeof(T)) && | |
EqualityComparer<T>.Default is EqualityComparer<T> defaultComparer) | |
return defaultComparer.Equals; | |
var type = typeof(T); | |
var (xParam, yParam) = (Parameter(type), Parameter(type)); | |
Expression compare; | |
if (type.GetGenericArguments().ElementAtOrDefault(0) is Type elementType && | |
typeof(IEnumerable<>).MakeGenericType(elementType) is Type enumerableType && | |
enumerableType.IsAssignableFrom(type)) | |
{ | |
GetComparer(elementType, out var comparer, out _); | |
var sequenceEqual = s_sequenceEqualMethod.MakeGenericMethod(elementType); | |
var callSequenceEqual = Call(null, sequenceEqual, xParam, yParam, comparer); | |
compare = IfThen(callSequenceEqual, s_returnTrue); | |
} | |
else | |
// compare = CompareAllInstanceFields(type, xParam, yParam); | |
compare = ComparePublicInstanceProperties(type, xParam, yParam); | |
var getType = s_getTypeMethod; | |
var compareType = Equal(Call(xParam, getType), Call(yParam, getType)); | |
compare = IfThenElse(IsFalse(compareType), s_returnFalse, compare); | |
var callReferenceEquals = Call(null, s_referenceEqualsMethod, xParam, yParam); | |
compare = IfThenElse(callReferenceEquals, s_returnTrue, compare); | |
var xIsDefault = Equal(xParam, Default(type)); | |
var yIsDefault = Equal(yParam, Default(type)); | |
var onlyXIsDefault = AndAlso(xIsDefault, IsFalse(yIsDefault)); | |
var onlyYIsDefault = AndAlso(IsFalse(xIsDefault), yIsDefault); | |
var themOneIsDefault = OrElse(onlyXIsDefault, onlyYIsDefault); | |
compare = IfThenElse(themOneIsDefault, s_returnFalse, compare); | |
return Lambda<Func<T?, T?, bool>>(Block(compare, s_returnLabel), new[] { xParam, yParam }).Compile(); | |
} | |
public static Func<object?, object?, bool> Build(Type type) | |
{ | |
GetComparer(type, out var comparer, out var method); | |
var (xParam, yParam) = (Parameter(typeof(object)), Parameter(typeof(object))); | |
var compare = Call(comparer, method, Convert(xParam, type), Convert(yParam, type)); | |
return Lambda<Func<object?, object?, bool>>(compare, xParam, yParam).Compile(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
// * Summary * | |
BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2) | |
12th Gen Intel Core i5-12400, 1 CPU, 12 logical and 6 physical cores | |
.NET SDK=6.0.413 | |
[Host] : .NET 6.0.21 (6.0.2123.36311), X64 RyuJIT AVX2 | |
DefaultJob : .NET 6.0.21 (6.0.2123.36311), X64 RyuJIT AVX2 | |
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | | |
|----------------------- |----------:|---------:|---------:|------:|--------:|-----:|-------:|----------:|------------:| | |
| CompareJsonTyped | 490.20 ns | 7.199 ns | 6.734 ns | 1.00 | 0.00 | 4 | 0.0792 | 752 B | 1.00 | | |
| CompareJsonAnon | 446.05 ns | 8.433 ns | 7.888 ns | 0.91 | 0.02 | 3 | 0.1206 | 1136 B | 1.51 | | |
| CompareExpressionTyped | 29.49 ns | 0.269 ns | 0.239 ns | 0.06 | 0.00 | 2 | - | - | 0.00 | | |
| CompareExpressionAnon | 23.62 ns | 0.379 ns | 0.355 ns | 0.05 | 0.00 | 1 | - | - | 0.00 | | |
*/ | |
using System; | |
using BenchmarkDotNet.Attributes; | |
namespace Benchmarks | |
{ | |
[MemoryDiagnoser] | |
[RankColumn] | |
public class TestDynamicComparer | |
{ | |
public static void Main(string[] args) => BenchmarkDotNet.Running.BenchmarkRunner.Run<TestDynamicComparer>(); | |
Sample? obj1; | |
Sample? obj2; | |
object? anon1; | |
object? anon2; | |
[GlobalSetup] | |
public void Setup() | |
{ | |
obj1 = new Sample { IntVal = 1, Text = "c0", Id = Guid.Empty, EndTime = DateTime.UnixEpoch }; | |
obj2 = new Sample { IntVal = 1, Text = string.Join("", 'c', '0'), EndTime = DateTime.UnixEpoch }; | |
anon1 = new { v = 3, o = new { d = "c0" } }; | |
anon2 = new { v = 2 + 1, o = new { d = "c0" } }; | |
} | |
[Benchmark(Baseline = true)] | |
public bool CompareJsonTyped() => JsonSerializer.Serialize(obj1) == JsonSerializer.Serialize(obj2); | |
[Benchmark] | |
public bool CompareJsonAnon() => JsonSerializer.Serialize(anon1) == JsonSerializer.Serialize(anon2); | |
[Benchmark] | |
public bool CompareExpressionTyped() => Comparer.IsEqual(obj1, obj2); | |
[Benchmark] | |
public bool CompareExpressionAnon() => Comparer.IsEqual(anon1, anon2); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
var obj1 = new Sample { IntVal = 1, Text = "c0", Id = Guid.Empty, EndTime = DateTime.UnixEpoch }; | |
var obj2 = new Sample { IntVal = 1, Text = string.Join("", 'c', '0'), EndTime = DateTime.UnixEpoch }; | |
var obj3 = new Sample { }; | |
var obj4 = default(Sample); | |
var anon1 = new { v = 3, o = new { d = obj1.Text } }; | |
var anon2 = new { v = 2 + 1, o = new { d = "c0" } }; | |
var list1 = new List<ushort> { 1, 3, 5, 7 }; | |
var list2 = new List<ushort> { 2, 4, 6, 8 }; | |
var list3 = list2.ConvertAll(i => (ushort)(i - 1)); | |
var array1 = new ushort[] { 1, 3, 5, 7 }; | |
var unixEpochManual = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | |
Action<object> output = Console.WriteLine; | |
Func<object?, object?, bool> equals = Comparer.IsEqual; | |
output("\n===== Typed ====="); | |
output($"equals(obj1, obj1): {equals(obj1, obj1)}"); // true | |
output($"equals(obj1, obj2): {equals(obj1, obj2)}"); // true | |
output($"equals(obj1, obj3): {equals(obj1, obj3)}"); // false | |
output($"equals(obj1, obj4): {equals(obj1, obj4)}"); // false | |
output($"equals(obj2, obj2): {equals(obj2, obj2)}"); // true | |
output($"equals(obj2, obj3): {equals(obj2, obj3)}"); // false | |
output($"equals(obj2, obj4): {equals(obj2, obj4)}"); // false | |
output($"equals(obj3, obj3): {equals(obj3, obj3)}"); // true | |
output($"equals(obj3, obj4): {equals(obj3, obj4)}"); // false | |
output($"equals(obj4, obj4): {equals(obj4, obj4)}"); // true | |
output("\n===== Struct ====="); | |
output($"equals(1f / 8f, .125f): {equals(1f / 8f, .125f)}"); // true | |
output($"equals(DateTime.UnixEpoch, unixEpochManual): {equals(DateTime.UnixEpoch, unixEpochManual)}"); // true | |
output($"equals(MathF.PI, Math.PI): {equals(MathF.PI, Math.PI)}"); // false, 位數精度不同 | |
output($"equals((a: 74737, b: 'msg'), (74737, 'msg')): {equals((a: 74737, b: "msg"), (74737, "msg"))}"); // true | |
output("\n===== Anonymous class ====="); | |
output($"equals(anon1, anon2): {equals(anon1, anon2)}"); // true | |
output($"equals(new {{ s = 3 }}, new {{ s = 0 }}): {equals(new { s = 3 }, new { s = 0 })}"); // false | |
output("\n===== Collection ====="); | |
output($"equals(list1, list1): {equals(list1, list1)}"); // true | |
output($"equals(list1, list2): {equals(list1, list2)}"); // false | |
output($"equals(list1, list3): {equals(list1, list3)}"); // true | |
output($"equals(list1, list2): {equals(list1, array1)}"); // false | |
output("\n(press any key to exit)"); | |
Console.ReadKey(); | |
public sealed class Sample | |
{ | |
public int IntVal { get; set; } | |
public string? Text { get; set; } | |
public Guid Id { get; set; } | |
public DateTime EndTime { get; init; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
看起來大部分的狀況都搞定了 :D