2009-06-17 15 views
13

Właśnie natknąłem się na coś bardzo dziwnego dla mnie: kiedy używasz metody Equals() na typie wartości (i jeśli ta metoda nie została przesłonięta, oczywiście) dostajesz coś bardzo powolnegojeden do jednego za pomocą refleksji! Tak jak w:C# - Metoda Value Type Equals - dlaczego kompilator używa odbicia?

public struct MyStruct{ 
    int i; 
} 

    (...) 

    MyStruct s, t; 
    s.i = 0; 
    t.i = 1; 
    if (s.Equals(t)) /* s.i will be compared to t.i via reflection here. */ 
     (...) 

Moje pytanie: dlaczego kompilator C# nie generuje prostej metody porównywania typów wartości? Coś jak (w definicji MyStruct za):

public override bool Equals(Object o){ 
     if (this.i == o.i) 
     return true; 
     else 
     return false; 
    } 

Kompilator wie, jakie są kierunki MyStruct w czasie kompilacji, dlaczego to czekać aż do czasu wykonywania wyliczyć pola MyStruct?

Bardzo dziwne dla mnie.

Dzięki :)

DODANE: Niestety, ja po prostu sobie sprawę, że, oczywiście, nie jest Equals Hasło język ale metoda Runtime ... Kompilator jest całkowicie nieświadomy tego sposobu. Czyni sens, aby użyć refleksji.

+0

„Aby użyć standardowego wdrażania równych, twój typ wartości muszą być zapakowane i podał jako przykład typu odniesienia System.ValueType. Równości metodę wykorzystuje następnie refleksji wykonać porównanie." - msdn.microsoft.com/en-us/library/ff647790.aspx – MrPhil

Odpowiedz

8

Poniżej znajduje się decompiled ValueType.Equals metody z mscorlib:

public override bool Equals(object obj) 
{ 
    if (obj == null) 
    { 
     return false; 
    } 
    RuntimeType type = (RuntimeType) base.GetType(); 
    RuntimeType type2 = (RuntimeType) obj.GetType(); 
    if (type2 != type) 
    { 
     return false; 
    } 
    object a = this; 
    if (CanCompareBits(this)) 
    { 
     return FastEqualsCheck(a, obj); 
    } 
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 
    for (int i = 0; i < fields.Length; i++) 
    { 
     object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(a, false); 
     object obj4 = ((RtFieldInfo) fields[i]).InternalGetValue(obj, false); 
     if (obj3 == null) 
     { 
      if (obj4 != null) 
      { 
       return false; 
      } 
     } 
     else if (!obj3.Equals(obj4)) 
     { 
      return false; 
     } 
    } 
    return true; 
} 

Jeśli to możliwe, dokonane zostanie porównanie bitowe (zwróć uwagę na CanCompareBits i FastEqualsCheck, które są zdefiniowane jako InternalCall.) JIT prawdopodobnie wprowadziłby tutaj odpowiedni kod. hy jest tak powolny, nie mogłem ci powiedzieć.

+0

Po prostu przetestuj to. Masz rację. :) – SRO

+2

Zastanawiam się, czy pojawiłyby się problemy z kompatybilnością, gdyby środowisko wykonawcze automatycznie generowało nadpisanie 'Equals' dla dowolnej struktury, która jeszcze nie zdefiniowała:' bool Equals (obiekt inny) {return StructComparer .EqualsProc (uzupełnij to, inne); } ', gdzie' EqualsProc' był statycznym polem dla delegata w klasie statycznej 'StructComparer '? Takie podejście pozwoliłoby uniknąć konieczności użycia refleksji za każdym razem, gdy obiekt jest porównywany, a także mogłoby uniknąć kroku boksu. – supercat

9

Nie używa refleksji , gdy nie jest konieczne. Po prostu porównuj wartości bit po bicie w przypadku, gdy może to zrobić struct. Jednakże, jeśli którykolwiek z członków struct (lub członków, potomkowie) zastąpi object.Equals i dostarczy własną implementację, oczywiście nie może polegać na porównaniu bit po bicie w celu obliczenia wartości zwracanej.

Powód jest powolny: parametr o numerze Equals należy do typu object, a typy wartości należy opakować, aby można było je traktować jako object. Boks polega na przydzielaniu pamięci na stercie i pamięci, kopiując typ wartości do tej lokalizacji.

Można ręcznie dostarczyć przeciążenie dla metody Equals że ma swoje własne struct jako parametr aby zapobiec boks:

public bool Equals(MyStruct obj) { 
    return obj.i == i; 
} 
+4

Używa refleksji w niektórych przypadkach. Jeśli wykryje, że może po prostu zabrudzić wyniki, robi to - ale jeśli istnieją pola referencyjne (lub typy zawierające typy odniesienia) w polach, musi to zrobić bardziej bolesny proces. –

+1

Kiedy to pisałem, czytałem i odkryłem, że niektórzy autorzy frameworków .Net (Cwalina, Abrams) potwierdzają, że Equals używa refleksji na temat typów wartości. Ale może właśnie w wersji 2.0? – SRO

+2

Sylvain: Mają rację. Jak powiedział Jon, jeśli struktura zawiera typy odniesienia jako członkowie, musi wywoływać Equals na tych polach. Zaktualizowałem odpowiedź, aby to odzwierciedlić. Chodzi mi o to, że nie używa refleksji, gdy tego nie potrzebuje (jak na przykład). –

3

Idea funkcji generowanej przez kompilator jest uzasadniona.

Pomyśl o efektach, ale myślę, że zespół projektujący język zrobił to dobrze. Zintegrowane metody znane z C++ są trudne do zrozumienia dla początkujących. Pozwala zobaczyć, co by się stało w C# z wygenerowane automatycznie struct.Equals:

Jak to jest teraz, pojęcie .equals() jest prosta:

  • Co dziedziczy struct Równa z ValueType.
  • W przypadku nadpisania stosuje się niestandardową metodę Równania.

Jeśli kompilator zawsze równa się stworzyć metodę, możemy mieć:

  • Co dziedziczy struct Równa Address. (ValueType nie będzie już realizować swoją własną wersję)
  • Object.Equals jest teraz zawsze (!) Nadpisane, albo przez kompilator generowanego Równa metody lub przez realizację użytkowników

Teraz nasza struktura ma wygenerowany automatycznie metodę nadpisywania którego czytnik kodów nie widzi! Skąd wiadomo, że metoda podstawowa Object.Equals nie ma zastosowania do struktury? Ucząc się wszystkich przypadków metod generowanych automatycznie przez kompilator. I to jest dokładnie jedno z obciążeń uczących się C++.

Uznalibyśmy za dobrą decyzję, aby pozostawić efektywną struct Equals użytkownikowi i zachować proste pojęcia, wymagające standardowej domyślnej metody Equals.

To powiedziane, krytyczne dla wydajności konstrukcje powinny zastąpić Równania. Poniższy kod pokazuje

vs 53 milisekund zmierzone na .Net 4.5.1

Ten przyrost wydajności jest na pewno ze względu na unikanie wirtualnych równi, ale w każdym razie, tak czy wirtualne Object.Equals byłoby zwany wzmocnieniem byłby znacznie niższy. Przypadki krytyczne dla wydajności nie będą nazywać Object.Equals, więc zastosowanie tutaj miałoby tutaj zastosowanie.

using System; 
using System.Diagnostics; 

struct A 
{ 
    public int X; 
    public int Y; 
} 

struct B : IEquatable<B> 
{ 
    public bool Equals(B other) 
    { 
     return this.X == other.X && this.Y == other.Y; 
    } 

    public override bool Equals(object obj) 
    { 
     return obj is B && Equals((B)obj); 
    } 

    public int X; 
    public int Y; 
} 


class Program 
{ 
    static void Main(string[] args) 
    { 
     var N = 100000000; 

     A a = new A(); 
     a.X = 73; 
     a.Y = 42; 
     A aa = new A(); 
     a.X = 173; 
     a.Y = 142; 

     var sw = Stopwatch.StartNew(); 
     for (int i = 0; i < N; i++) 
     { 
      if (a.Equals(aa)) 
      { 
       Console.WriteLine("never ever"); 
      } 
     } 
     sw.Stop(); 
     Console.WriteLine(sw.ElapsedMilliseconds); 

     B b = new B(); 
     b.X = 73; 
     b.Y = 42; 
     B bb = new B(); 
     b.X = 173; 
     b.Y = 142; 

     sw = Stopwatch.StartNew(); 
     for (int i = 0; i < N; i++) 
     { 
      if (b.Equals(bb)) 
      { 
       Console.WriteLine("never ever"); 
      } 
     } 
     sw.Stop(); 
     Console.WriteLine(sw.ElapsedMilliseconds); 
    } 
} 

zobaczyć również http://blog.martindoms.com/2011/01/03/c-tip-override-equals-on-value-types-for-better-performance/

+0

Warto zauważyć, że kompilator nie używa Reflection; po prostu wykorzystuje metodę wirtualną do metody "ValueType.Equals"; ponieważ ta metoda oczekuje, że 'this' będzie typem klasy [' ValueType' jest, pomimo swojej nazwy, klasą], wartość musi zostać zapakowana. Koncepcyjnie byłoby miło, gdyby "ValueType" zdefiniował statyczną metodę "ValueTypeEquals (ref T it, Object other) {ValueTypeComparer . Porównać (ref, inne);' i zalecił, że gdy to możliwe kompilatory nazywają to preferencją do wirtualnej metody "równości". Takie podejście mogłoby ... – supercat

+0

... umożliwić środowisku wykonawczemu generowanie porównań dla każdego typu tylko przy pierwszym użyciu, a następnie uzyskać dostęp do porównywalnika bezpośrednio podczas kolejnych wywołań. – supercat

+1

Nitpick: Wywołanie 'ReferenceEquals (null, obj)' jest technicznie redundantne, ponieważ wyrażenie "jest" zwraca wartość "false", jeśli podane wyrażenie ("obj") ma wartość null] (https://msdn.microsoft.com/en -us/library/scekt9xw.aspx). Jestem pewien, że nie ma to wpływu na wyniki benchmarku w jakikolwiek użyteczny sposób. Nadal nie polegałbym na kompilatorze, aby go zoptymalizować, gdyby to miało kiedykolwiek znaczenie. – tne

Powiązane problemy