Frage an struct, compiler-construction, c# – Value Type Equals-Methode - warum verwendet der Compiler Reflexion?

15

Ich bin gerade auf etwas Seltsames gestoßen: Wenn Sie die Equals () -Methode für einen Werttyp verwenden (und diese Methode natürlich nicht überschrieben wurde), erhalten Sie etwassehr, sehr langsame Felder werden eins zu eins durch Reflektion verglichen! Wie in :

<code>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. */
      (...)
</code>

Meine Frage: Warum generiert der C # -Compiler keine einfache Methode zum Vergleichen von Werttypen? So etwas wie (in der Definition von MyStruct):

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

Der Compiler weiß, welche Felder MyStruct zur Kompilierungszeit enthält. Warum wartet er bis zur Laufzeit, um MyStruct-Felder aufzulisten?

Sehr seltsam für mich.

Vielen Dank :)

HINZUGEFÜGT : Sorry, mir ist nur klar, dass natürlichEquals ist kein Sprachschlüsselwort, sondern eine Laufzeitmethode ... Dem Compiler ist diese Methode überhaupt nicht bekannt. Es ist also sinnvoll, hier Reflexion zu verwenden.

"Um die Standardimplementierung von Equals zu verwenden, muss Ihr Werttyp als Instanz des Referenztyps System.ValueType angegeben und übergeben werden. Die Equals-Methode verwendet dann Reflection, um den Vergleich durchzuführen." - msdn.microsoft.com/en-us/library/ff647790.aspx MrPhil

Deine Antwort

3   die antwort
10

wenn es nicht sein muss. Es vergleicht nur die Werte Stück für Stück, falls diestruct wenn es das kann. Es gibt jedoch, wenn einer derstruct Mitglieder (oder Mitglieder von Mitgliedern, Nachkommen) überschreibenobject.Equals und bietet eine eigene Implementierung, offensichtlich kann es sich nicht auf einen bitweisen Vergleich stützen, um den Rückgabewert zu berechnen.

Der Grund dafür ist, dass der Parameter zu langsam istEquals ist vom Typobject und Werttypen müssen mit einem Kästchen versehen werden, um alsobject. Beim Boxen wird Speicher auf dem Heap zugewiesen und der Wertetyp an diesen Speicherort kopiert.

Sie können manuell eine Überladung für die bereitstellenEquals Methode, die Ihre eigenen nimmtstruct als Parameter um Boxen zu verhindern:

public bool Equals(MyStruct obj) {
     return obj.i == i;
}
In einigen Fällen wird Reflexion verwendet. Wenn es erkennt, dass es die Ergebnisse nur verfälschen kann, tut es dies. Wenn es jedoch Referenztypen (oder Typen mit Referenztypen) in den Feldern gibt, muss es einen schmerzhafteren Prozess ausführen. Jon Skeet
Sylvain: Sie haben recht. Wie Jon sagte, wenn die Struktur Referenztypen als Member enthält, muss sie in diesen Feldern Equals aufrufen. Ich habe die Antwort entsprechend aktualisiert. Der Punkt, den ich ansprechen wollte, ist, dass es keine Reflexion verwendet, wenn es nicht benötigt wird (wie in Ihrem Beispiel). Mehrdad Afshari
Snarfblam - Ich wundere mich immer noch über das Gleiche. Sylvain Rodrigue
@Snarfblam: Aus dem von Mehrdad veröffentlichten Code geht hervor, dass er für jedes der in der Struktur enthaltenen referenzierten Objekte Equals aufruft - fast rekursiv. Wenn zwei Objekte gleich sind, ihre Referenzen jedoch nicht, schlägt ein bitweiser Vergleich fehl. Samir Talwar
3

Wenn ich über die Effekte nachdenke, denke ich, dass das Sprachdesign-Team es richtig gemacht hat. Aus C ++ bekannte compilergenerierte Methoden sind für Anfänger schwer zu verstehen. Mal sehen, was in C # mit automatisch generierten struct.Equals passieren würde:

So wie es jetzt ist, ist das Konzept von .Equals () einfach:

Jede Struktur erbt Equals von ValueType.Beim Überschreiben wird die benutzerdefinierte Equals-Methode angewendet.

Wenn der Compiler immer die Equals-Methode erstellen würde, hätten wir möglicherweise:

Jede Struktur erbt Equals von Object. (ValueType implementiert keine eigene Version mehr)Object.Equals wird jetzt immer (!) Überschrieben, entweder durch die vom Compiler generierte Equals-Methode oder durch die Implementierung des Benutzers

Jetzt hat unsere Struktur eine automatisch generierte Überschreibungsmethode, die der Codeleser nicht sieht! Woher wissen Sie also, dass die Basismethode Object.Equals nicht für Ihre Struktur gilt? Durch das Erlernen aller Fälle von automatisch generierten Compiler-Methoden. Und dies ist genau eine der Belastungen, die das Erlernen von C ++ mit sich bringen.

Es wäre eine gute Entscheidung, effiziente struct Equals dem Benutzer zu überlassen und die Konzepte einfach zu halten, wobei eine standardmäßige Equals-Standardmethode erforderlich ist.

Allerdings sollten leistungskritische Strukturen Equals überschreiben. Der folgende Code zeigt

3606 vs53 Millisekunden gemessen unter .Net 4.5.1

Dieser Leistungsgewinn ist sicherlich darauf zurückzuführen, dass virtuelle Equals vermieden werden. Wenn also das virtuelle Object.Equals aufgerufen wird, ist der Gewinn viel geringer. Leistungskritische Fälle rufen jedoch nicht Object.Equals auf, sodass hier der Gewinn gilt.

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

siehe auchhttp://blog.martindoms.com/2011/01/03/c-tip-override-equals-on-value-types-for-better-performance/

... ermöglichte der Laufzeit, nur bei der ersten Verwendung einen Vergleicher für jeden Typ zu generieren und dann bei nachfolgenden Aufrufen direkt auf diesen Vergleicher zuzugreifen. supercat
@tne: so richtig! entfernte es. citykid
Es ist erwähnenswert, dass der Compiler Reflection nicht verwendet. Es wird einfach der Versand virtueller Methoden an eine Methode verwendetValueType.Equals; weil diese Methode erwartetthis ein Klassentyp seinValueType Ist, trotz seines Namens, eine Klasse.] Der Wert muss in ein Kästchen gesetzt werden. Konzeptionell wäre es vielleicht schön gewesen, wennValueType hatte eine statische Methode definiertValueTypeEquals<T>(ref T it, Object other) { ValueTypeComparer<T>.Compare(ref it, other); und empfohlen, dass Compiler, wenn möglich, dies der virtuellen vorziehenEquals Methode. Ein solcher Ansatz hätte ... supercat
Nitpick: DieReferenceEquals(null, obj) Anruf ist technisch redundant wiedasis expression ergibt false, wenn der angegebene Ausdruck (obj) ist Null. Ich bin sicher, dass es die Ergebnisse des Benchmarks in keiner nützlichen Weise beeinflusst. Trotzdem würde ich mich nicht darauf verlassen, dass der Compiler es optimiert, wenn es jemals darauf ankommt. tne
10

ib:

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

Wenn möglich, wird ein bitweiser Vergleich durchgeführt (beachten Sie CanCompareBits und FastEqualsCheck, die beide als InternalCall definiert sind. Das JIT würde vermutlich den entsprechenden Code hier einfügen. Warum es so langsam ist, kann ich Ihnen nicht sagen .

Testen Sie das einfach. Du hast recht. :) Sylvain Rodrigue
Ich frage mich, ob es irgendwelche Kompatibilitätsprobleme geben würde, wenn die Laufzeit automatisch eine generieren würdeEquals Überschreiben Sie für jede Struktur, die noch keine definiert hat:bool Equals(object other) { return StructComparer<thisType>.EqualsProc(ref this, other); }, woherEqualsProc war ein statisches Delegatenfeld innerhalb einer statischen KlasseStructComparer<thisType>? Bei einem solchen Ansatz müsste nicht jedes Mal Reflection verwendet werden, wenn ein Objekt verglichen wird, und es könnte auch ein Boxschritt vermieden werden. supercat

Verwandte Fragen