2014-01-13 14 views
25

Dies ist etwas, das ich bis heute nicht bemerkt hatte. Offensichtlich verursacht die .NET-Implementierung der vielbenutzten Tuple-Klassen (Tuple<T>, Tuple<T1, T2> usw.) bei der Ausführung gleichheitsbasierter Operationen die Boxstrafen für die Werttypen..NET Tuple und Gleichwertige Leistung

Hier ist, wie die Klasse Art im Rahmen (Quelle über ILSpy) implementiert:

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; 
    } 
} 

Das Problem, das ich sehe, ist es für Equals Anrufe eines zweistufiger Boxen-Unboxing, sagt verursacht, ein, an der comparer.Equals welche boxen den artikel, zwei, die EqualityComparer<object> ruft die nicht-generischeEquals die wiederum intern haben, um das einzelteil zu orginal type.

Stattdessen warum würden sie nicht so etwas wie:

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; 
} 

Ich war überrascht, Gleichheit realisiert auf diese Weise in .NET Tupel-Klasse zu sehen. Ich benutzte den Tupeltyp als Schlüssel in einem der Wörterbücher.

Gibt es einen Grund, warum dies implementiert werden muss, wie im ersten Code gezeigt? Es ist ein wenig entmutigend, in diesem Fall von dieser Klasse Gebrauch zu machen.

Ich glaube nicht, Code-Refactoring und nicht duplizierenden Daten sollten die Hauptsorgen gewesen sein. Die gleiche nicht-generische/boxende Implementierung ist hinter IStructuralComparable auch gegangen, aber da IStructuralComparable.CompareTo weniger verwendet wird, ist es kein Problem oft.


gebenchmarkt ich die beiden oben genannten Ansätze mit einem dritten Ansatz, der noch weniger belastend ist, wie dies (nur das Wesentliche):

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); 
} 

für ein paar Tuple<DateTime, DateTime> Felder ein 1000000 Equals Anrufe. Dies ist das Ergebnis:

erster Ansatz (original .NET-Implementierung) - 310 ms

2. Ansatz - 60 ms

3. Ansatz - 130 ms

Die Standardimplementierung ist etwa 4-5 mal langsamer als die optimale Lösung.

+1

Ich sehe keinen Grund, warum 'IEqualityComparer ' verwendet wurde, aber ich sage nicht, dass es keine gibt. Aber du kannst es immer noch ein bisschen besser machen: http://pastebin.com/tNA2FYjq – MarcinJuraszek

+1

@MarcinJuraszek Wie ist das besser? 'Tuple <,>' implementiert 'IStructuralEquatable' mit diesen Definitionen' bool Equals (object other, IEqualityComparer-Vergleicher); int GetHashCode (IEqualityComparer-Vergleicher); ' – nawfal

+1

" ... viel benutzte Tuple-Klassen ... ". Da ist dein Problem! – Gusdor

Antwort

10

Sie haben sich gefragt, ob es auf diese Weise implementiert werden soll. Kurz gesagt, ich würde nein sagen: Es gibt viele funktional äquivalente Implementierungen.

Aber warum macht die vorhandene Implementierung eine solche explizite Verwendung von EqualityComparer<object>.Default? Es kann nur ein Fall der Person sein, die dies mental geschrieben hat und für das "falsche" oder zumindest andere Ding optimiert hat, als Ihr Szenario der Geschwindigkeit in einer inneren Schleife. Je nach Benchmark scheint es das Richtige zu sein.

Aber welches Benchmark-Szenario könnte sie dazu bringen, diese Wahl zu treffen? Nun, die Optimierung, auf die sie abzielten, scheint darin zu bestehen, die minimale Anzahl von Klasseninstanzen der EqualityComparer-Klasse zu optimieren.Wahrscheinlich wählen sie dies, weil die Template-Instanziierung mit Speicher- oder Ladezeitkosten verbunden ist. Wenn dies der Fall ist, können wir davon ausgehen, dass ihr Benchmarkszenario eher auf der Startzeit der App oder der Speicherbelegung als auf einem Szenario mit engen Schleifen basieren könnte.

Hier ist ein Wissenspunkt zur Unterstützung der Theorie (gefunden mit Bestätigungsfehler :) - EqualityComparer Implementierungen Methode Körper können nicht geteilt werden, wenn T eine Struktur ist. Auszug aus http://blogs.microsoft.co.il/sasha/2012/09/18/runtime-representation-of-genericspart-2/

Wenn die CLR muss eine Instanz einer geschlossenen gattungsgemäßen Art zu schaffen, wie Liste, erstellt es eine Methodentabelle und EEClass basierend auf den offenen Typ. Wie immer enthält die Methodentabelle Methodenzeiger, die im laufenden Betrieb vom JIT-Compiler kompiliert werden. Allerdings gibt es eine entscheidende Optimierung hier: kompilierte Methodenrümpfe auf geschlossene generic Typen, die Referenztyp Parameter können gemeinsam genutzt werden. [...] Die gleiche Idee nicht für Werttypen funktioniert. Wenn zum Beispiel T lang ist, die Zuordnungsanweisung Artikel [size] = item erfordert eine andere Anweisung, weil 8 Bytes müssen statt 4. Auch größere Werttypen kann sogar mehr erfordern als ein Befehl kopiert werden; und so weiter.

+0

Gute Antwort auf andere mögliche Überlegungen. Ich werde das jetzt akzeptieren. – nawfal

+0

@nawfal Danke! Ich hatte Spaß, diesen zu erforschen! –