2009-06-17 12 views
13

Mi sono imbattuto in qualcosa di molto strano per me: quando si usa il metodo Equals() su un tipo di valore (e se questo metodo non è stato sovrascritto, ovviamente) si ottiene qualcosa molto molto lento - i campi vengono confrontati uno a uno usando la riflessione! Come in:C# - Metodo di tipo Valore uguale: perché il compilatore utilizza il riflesso?

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

La mia domanda: perché il compilatore C# non generano un metodo semplice per confrontare i tipi di valore? Qualcosa di simile (nella definizione di MyStruct):

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

Il compilatore sa quali sono i campi di MyStruct in fase di compilazione, il motivo per cui ci si aspetta fino al runtime per enumerare i campi MyStruct?

Molto strano per me.

Grazie :)

aggiunto: Siamo spiacenti, ho appena si rendono conto che, ovviamente, Equals non è una parola chiave lingua, ma un metodo di esecuzione ... Il compilatore è completamente all'oscuro di questo metodo. Quindi fa sentire qui per usare la riflessione.

+0

"Per utilizzare l'implementazione standard di Eguali, il tipo di valore deve essere imballato e passato come esempio del tipo riferimento System.ValueType. Il metodo Equals quindi utilizza riflessione per eseguire la confronto." - msdn.microsoft.com/en-us/library/ff647790.aspx – MrPhil

risposta

8

Di seguito è riportato il metodo ValueType.Equals decompilato da 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; 
} 

Se possibile, verrà eseguito un confronto bit-saggio (notare CanCompareBits e FastEqualsCheck, entrambi definiti come InternalCall. In questo caso, il JIT inserirà presumibilmente il codice appropriato. è così lento, non potrei dirtelo

+0

Basta testarlo. Hai ragione. :) – SRO

+2

Mi chiedo se non ci sarebbe alcun problema di compatibilità se il runtime dovesse generare automaticamente un 'esclusione Equals' per qualsiasi struct che non ha già definire uno:' bool Equals (Object altro) {return StructComparer .EqualsProc (ref this, other); } ', dove' EqualsProc' era un campo delegato statico nella classe statica 'StructComparer '? Tale approccio eviterebbe di dover utilizzare Reflection ogni volta che viene confrontato un oggetto e potrebbe anche evitare un passaggio di boxe. – supercat

9

Non utilizza la riflessione quando non è necessario. Confronta i valori bit per bit nel caso in cui lo sia struct. Tuttavia, se uno qualsiasi dei membri struct (o membri di membri, eventuali discendenti) ha la precedenza su object.Equals e fornisce la propria implementazione, ovviamente, non può fare affidamento sul confronto bit per bit per calcolare il valore di ritorno.

Il motivo per cui è lento è che il parametro su Equals è di tipo object e i tipi di valore devono essere racchiusi come object. La boxe comporta l'allocazione della memoria sull'heap e la memoria copiando il tipo di valore in quella posizione.

Si potrebbe fornire manualmente un sovraccarico per il metodo Equals che prende il proprio struct come parametro per evitare che la boxe:

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

In alcuni casi utilizza la riflessione. Se rileva che può solo fondere i risultati, lo fa - ma se ci sono tipi di riferimento (o tipi che contengono tipi di riferimento) nei campi, deve fare un processo più doloroso. –

+1

Mentre stavo scrivendo questo, ho letto e ho scoperto che alcuni autori di framework .Net (Cwalina, Abrams) confermano che Equals sta usando la riflessione sui tipi di valore. Ma forse solo nel Framework 2.0? – SRO

+2

Sylvain: Hanno ragione. Come ha detto Jon, se la struttura contiene tipi di riferimento come membri, deve chiamare Equals su quei campi. Ho aggiornato la risposta per riflettere questo. Il punto che stavo cercando di fare è che non usa il riflesso quando non ne ha bisogno (come nel tuo esempio). –

3

L'idea di una funzione generata dal compilatore è giustificata.

Pensando agli effetti, penso che il team di progettazione linguistica abbia fatto bene. I metodi Compilergenerated noti da C++ sono difficili da capire per i principianti. Consente di vedere cosa sarebbe successo in C# con struct.Equals generati automaticamente:

Come è ora, il concetto di .equals() è semplice:

  • Ogni eredita struct Uguale da ValueType.
  • Se sovrascritto, si applica il metodo personalizzato Equals.

Se il compilatore sarebbe sempre creare il metodo Equals, potremmo avere:

  • Ogni eredita struct Uguale da Object. (ValueType non sarebbe più implementare la propria versione)
  • Object.Equals è ora sempre (!) Sovrascritto, sia dal compilatore generato metodo Equals o l'attuazione utenti

Ora la nostra struttura ha un metodo di sostituzione generata automaticamente che il lettore di codice non vede! Quindi, come fai a sapere che il metodo base Object.Equals non si applica alla tua struct? Imparando tutti i casi di metodi generati dal compilatore automatico. E questo è esattamente uno degli oneri che impara il C++.

considererebbe buona decisione di lasciare struct efficiente Uguale per l'utente e mantenere i concetti semplici, che richiedono un livello di default Equals metodo.

Detto questo, le strutture di prestazioni critiche dovrebbero ignorare Equals. Il codice seguente mostra

vs 53 millisecondi misurate su Net 4.5.1

Questo miglioramento delle prestazioni è certamente dovuto alla evitando Eguali virtuali, ma comunque, quindi se i Object.Equals virtuali sarebbero chiamato il guadagno sarebbe molto più basso. Tuttavia, i casi critici per le prestazioni non chiamano Object.Equals, pertanto il guadagno qui si applica.

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

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

+0

Vale la pena notare che il compilatore non usa Reflection; semplicemente usa l'invio del metodo virtuale a un metodo 'ValueType.Equals'; perché questo metodo si aspetta che 'questo' sia un tipo di classe [' ValueType' è, nonostante il suo nome, una classe] il valore deve essere racchiuso. Concettualmente, potrebbe essere stato bello se 'ValueType' avesse definito un metodo statico' ValueTypeEquals (ref T, Object other) {ValueTypeComparer .Compare (ref, other); 'e raccomandato che, quando possibile, i compilatori lo chiamino a preferenza al metodo virtuale 'Equals'. Un simile approccio potrebbe ... – supercat

+0

... abilitare il runtime a generare un confronto per ogni tipo solo al primo utilizzo e quindi accedere a tale comparatore direttamente sulle successive chiamate. – supercat

+1

Nitpick: la chiamata 'ReferenceEquals (null, obj)' è tecnicamente ridondante poiché [l'espressione 'is' restituisce false se l'espressione fornita (' obj') è null] (https://msdn.microsoft.com/en -us/library/scekt9xw.aspx). Sono sicuro che non influisce comunque sui risultati del benchmark in alcun modo utile. Tuttavia, non mi affiderei al compilatore per ottimizzarlo se dovesse mai avere importanza. – tne

Problemi correlati