.NET Tuple 和 Equals 性能
這是我直到今天才注意到的。顯然,當執行基於相等的操作時
Tuple<T>,經常使用的元組類(等)的 .NET 實現會導致值類型Tuple<T1, T2>的裝箱懲罰。以下是該類在框架中的實現方式(來自 ILSpy 的原始碼):
public class Tuple<T1, T2> : IStructuralEquatable { public T1 Item1 { get; private set; } public T2 Item2 { get; private set; } public Tuple(T1 item1, T2 item2) { this.Item1 = item1; this.Item2 = item2; } public override bool Equals(object obj) { return this.Equals(obj, EqualityComparer<object>.Default); } public override int GetHashCode() { return this.GetHashCode(EqualityComparer<object>.Default); } public bool Equals(object obj, IEqualityComparer comparer) { if (obj == null) { return false; } var tuple = obj as Tuple<T1, T2>; return tuple != null && comparer.Equals(this.Item1, tuple.Item1) && comparer.Equals(this.Item2, tuple.Item2); } public int GetHashCode(IEqualityComparer comparer) { int h1 = comparer.GetHashCode(this.Item1); int h2 = comparer.GetHashCode(this.Item2); return (h1 << 5) + h1 ^ h2; } }我看到的問題是它會導致兩個階段的裝箱-拆箱,比如
Equals呼叫,一是在comparer.Equals哪個盒子上裝箱,二是EqualityComparer<object>呼叫非泛型Equals,而非泛型的呼叫又必須在內部將項目拆箱為原始類型。相反,他們為什麼不做類似的事情:
public override bool Equals(object obj) { var tuple = obj as Tuple<T1, T2>; return tuple != null && EqualityComparer<T1>.Default.Equals(this.Item1, tuple.Item1) && EqualityComparer<T2>.Default.Equals(this.Item2, tuple.Item2); } public override int GetHashCode() { int h1 = EqualityComparer<T1>.Default.GetHashCode(this.Item1); int h2 = EqualityComparer<T2>.Default.GetHashCode(this.Item2); return (h1 << 5) + h1 ^ h2; } public bool Equals(object obj, IEqualityComparer comparer) { var tuple = obj as Tuple<T1, T2>; return tuple != null && comparer.Equals(this.Item1, tuple.Item1) && comparer.Equals(this.Item2, tuple.Item2); } public int GetHashCode(IEqualityComparer comparer) { int h1 = comparer.GetHashCode(this.Item1); int h2 = comparer.GetHashCode(this.Item2); return (h1 << 5) + h1 ^ h2; }我很驚訝地看到在 .NET 元組類中以這種方式實現了相等性。我在其中一個字典中使用元組類型作為鍵。
**是否有任何理由必須按照第一個程式碼中所示的方式實現這一點?**在這種情況下使用這個類有點令人沮喪。
我不認為程式碼重構和非重複數據應該是主要問題。同樣的非泛型/裝箱實現也落後
IStructuralComparable了,但由於IStructuralComparable.CompareTo使用較少,所以它經常不是問題。我用第三種方法對上述兩種方法進行了基準測試,這種方法仍然不那麼費力,就像這樣(只有要領):
public override bool Equals(object obj) { return this.Equals(obj, EqualityComparer<T1>.Default, EqualityComparer<T2>.Default); } public bool Equals(object obj, IEqualityComparer comparer) { return this.Equals(obj, comparer, comparer); } private bool Equals(object obj, IEqualityComparer comparer1, IEqualityComparer comparer2) { var tuple = obj as Tuple<T1, T2>; return tuple != null && comparer1.Equals(this.Item1, tuple.Item1) && comparer2.Equals(this.Item2, tuple.Item2); }對於幾個
Tuple<DateTime, DateTime>欄位,有 1000000 次Equals呼叫。這是結果:第一種方法(原始 .NET 實現)- 310 毫秒
第二種方法 - 60 毫秒
第三種方法 - 130 毫秒
預設實現比最優解決方案慢大約 4-5 倍。
您想知道它是否“必須”以這種方式實施。簡而言之,我會說不:有許多功能等效的實現。
但是為什麼現有的實現會如此明確地使用
EqualityComparer<object>.Default? 這可能只是寫這篇文章的人在心理上針對“錯誤”進行優化的人的情況,或者至少與您在內部循環中的速度場景不同。根據他們的基準,它可能看起來是“正確”的事情。但是,什麼樣的基準情景會導致他們做出這樣的選擇呢?好吧,他們所針對的優化似乎是針對最少數量的 EqualityComparer 類模板實例化進行優化。他們可能會選擇這個,因為模板實例化會帶來記憶體或載入時間成本。如果是這樣,我們可以猜測他們的基準場景可能是基於應用程序啟動時間或記憶體使用情況,而不是一些緊密循環的場景。
這是支持該理論的一個知識點(通過使用確認偏差發現:) -如果 T 是 struct ,則無法共享 EqualityComparer 實現方法體。摘自<http://blogs.microsoft.co.il/sasha/2012/09/18/runtime-representation-of-genericspart-2/>
當 CLR 需要創建一個封閉的泛型類型的實例時,例如 List,它會基於開放的類型創建一個方法表和 EEClass。與往常一樣,方法表包含由 JIT 編譯器動態編譯的方法指針。但是,這裡有一個關鍵的優化:可以共享在具有引用類型參數的封閉泛型類型上編譯的方法體。[…]同樣的想法不適用於值類型。例如,當 T 很長時,賦值語句 items[size] = item 需要不同的指令,因為必須複製 8 個字節而不是 4 個字節。更大的值類型甚至可能需要多條指令;等等。