2009-04-09 9 views
18

Esiste un sovraccarico aggiuntivo nell'uso dei versi del metodo object.ReferenceEquals utilizzando ((object)obj1 == (object)obj2)?Differenze di rendimento di uguaglianza di riferimento? ((oggetto) obj1 == (oggetto) obj2) vs. oggetto.ReferenceEquals (obj1, obj2)

Nel primo caso, sarebbe implicata una chiamata al metodo statico, e in entrambi i casi verrebbe coinvolta una forma di lancio su un oggetto.

Anche se il compilatore esegue il bilanciamento di questi metodi, per quanto riguarda la disuguaglianza?

(object)obj != null 

rispetto al ...

!object.ReferenceEquals(obj,null) 

Suppongo che a un certo punto, si sarebbe verificato un negazione logica, sia all'interno della Operatore! =, O applicata al risultato dei ReferenceEquals metodo. Cosa ne pensi?

C'è anche il problema della leggibilità da considerare. ReferenceEquals sembra più chiaro quando si controlla l'uguaglianza, ma per la disuguaglianza, si potrebbe perdere lo ! precedente object.ReferenceEquals, mentre lo != nella prima variante è difficile da trascurare.

risposta

21

C'è overhead aggiuntivo utilizzando il metodo object.ReferenceEquals

No. Il metodo contiene direttamente la descrizione minima IL effettuare il controllo di uguaglianza di riferimento (per la cronaca: è equivalente all'operatore di VB Is) e sarà spesso delineato dalla JIT (in particolare quando si sceglie il targeting x64), quindi c'è il no overhead.

Informazioni sulla leggibilità: personalmente penso che object.ReferenceEquals sia potenzialmente più leggibile (anche in forma negata) perché esprime esplicitamente la sua semantica. Il cast su object potrebbe confondere alcuni programmatori.

Ho appena trovato an article discutendo di questo. Preferisce (object)x == y perché l'ingombro di IL è inferiore. Sostiene che questo potrebbe facilitare l'allineamento del metodo X usando questo confronto. Tuttavia (senza alcuna conoscenza dettagliata del JIT ma logicamente e intuitivamente) credo che questo sia sbagliato: se il JIT si comporta come un compilatore C++ ottimizzante, considererà il metodo dopo inlining la chiamata a ReferenceEquals, quindi (per il bene del metodo di allineamento X) l'impronta di memoria sarà esattamente la stessa in entrambi i casi.

Vale a dire: scegliere una via sull'altra non avrà alcun impatto sulla JIT e di conseguenza sulle prestazioni.

+3

+1, hanno concordato la leggibilità. – JaredPar

+0

Sono d'accordo che il cast per l'oggetto è confuso. Ci si può chiedere perché sia ​​necessario il cast in ((oggetto) obj == null), non rendendosi conto che l'uguaglianza di riferimento si riduce in definitiva a (oggetto == oggetto). Inoltre, il confronto con null sembra implicitamente un confronto di riferimento, ma l'operatore tipizzato viene effettivamente scelto. – Triynko

+0

Concordato sulla leggibilità, ma vedere la mia risposta sulle prestazioni. – nawfal

3

L'overhead di Object.ReferenceEquals è solo nel caricamento degli argomenti, che verranno eliminati JITted nella maggior parte degli scenari. Dopodiché, sia Object.ReferenceEquals che operator == arrivano a un singolo operatore ILQ. Nel peggiore dei casi, la differenza sarà insignificante.

Ancora più importante, Object.ReferenceEquals è molto più indicativo di (oggetto) o1 == (oggetto) o2. Indica chiaramente nel codice "Sto testando l'uguaglianza/identità di riferimento", piuttosto che nascondere l'intento sotto una serie di calchi.

+0

Concordo sul fatto che i cast potrebbero essere fuorvianti, ma non pensate che l'operatore '==' sia universalmente riconosciuto come confronto di riferimento? –

+5

No, al contrario. Ad esempio, string1 == stringa2 esegue un confronto di valori anziché un confronto di riferimento. Da qui la necessità del poster originale di eseguire il cast dell'oggetto per forzare un confronto di riferimento. – itowlson

+0

Re la tua prima frase: credo che sia esattamente il contrario (almeno questo è il modo in cui è implementato in Rotor), ma non importa. –

2

Il numero article about == operator menzionato in precedenza fornisce informazioni incomplete, almeno su .NET 4.0 (beh, è ​​stato scritto di nuovo in 2.0 volte).

Indica che ReferenceEquals non viene ottimizzato/inline, il che è vero solo se si crea il progetto con la configurazione 'AnyCPU'. L'impostazione su 'x86' o 'x64' rende (oggetto) obj == null e ReferenceEquals (oggetto, null) finiscono per essere identici IL, dove entrambi i metodi sono semplicemente un'istruzione IL 'ceq'.

Quindi la risposta è: l'IL prodotto da entrambi i metodi è identico su versioni Release/x86 o x64.

ReferenceEquals è decisamente più leggibile, almeno per i miei gusti.

+0

Anche se si tratta di differenze trascurabili, c'è comunque qualche differenza .. Vedi la mia risposta che è sulla piattaforma x86 .. – nawfal

4

Contrariamente alle risposte qui, ho trovato (object) == più veloce di object.ReferenceEquals. Quanto a quanto più veloce, molto trascurabile!

banco di prova:

So che avete bisogno controllo di uguaglianza di riferimento, ma sto tra cui metodo statico object.Equals(,) nonché in caso di classi in cui la sua non sovrascritto.

Piattaforma: x86; Configurazione: accumulo di uscita

class Person { 
} 

public static void Benchmark(Action method, int iterations = 10000) 
{ 
    Stopwatch sw = new Stopwatch(); 
    sw.Start(); 
    for (int i = 0; i < iterations; i++) 
     method(); 

    sw.Stop(); 
    MsgBox.ShowDialog(sw.Elapsed.TotalMilliseconds.ToString()); 
} 

prova:

Person p1 = new Person(); 
Person p2 = new Person(); 
bool b; 
Benchmark(() => 
{ 
    b = (object)p1 == (object)p2; //960 ~ 1000ms 
    b = object.ReferenceEquals(p1, p2); //~ 1250ms 
    b = object.Equals(p1, p2); //2100ms 
    b = EqualityComparer<Person>.Default.Equals(p1, p2); //~4000ms 

}, 100000000); 

Person p1 = new Person(); 
Person p2 = null; 
bool b; 
Benchmark(() => 
{ 
    b = (object)p1 == (object)p2; //990 ~ 1000ms 
    b = object.ReferenceEquals(p1, p2); // 1230 ~ 1260ms 
    b = object.Equals(p1, p2); //1250 ~ 1300ms 
    b = EqualityComparer<Person>.Default.Equals(p1, p2); //~3100ms 

}, 100000000); 

Person p1 = null; 
Person p2 = null; 
bool b; 
Benchmark(() => 
{ 
    b = (object)p1 == (object)p2; //960 ~ 1000ms 
    b = object.ReferenceEquals(p1, p2); //1260 ~ 1270ms 
    b = object.Equals(p1, p2); //1180 ~ 1220ms 
    b = EqualityComparer<Person>.Default.Equals(p1, p2); //~3100ms 

}, 100000000); 

Person p1 = new Person(); 
Person p2 = p1; 
bool b; 
Benchmark(() => 
{ 
    b = (object)p1 == (object)p2; //960 ~ 1000ms 
    b = object.ReferenceEquals(p1, p2); //1260 ~ 1280ms 
    b = object.Equals(p1, p2); //1150 ~ 1200ms 
    b = EqualityComparer<Person>.Default.Equals(p1, p2); //3700 ~ 3800ms 

}, 100000000); 

object.Equals(,) chiamate ReferenceEquals internamente e se non sono uguali sarebbe chiamare il Equals metodo virtuale overriding della classe, e quindi si vede l'avviso la differenza di velocità.

I risultati sono stati coerenti nel Debug configurazione troppo ...

Come sottolineato, l'accento dovrebbe essere sulla leggibilità/significatività/intento rivelando.

+2

Come quasi tutti i microbenchmark pubblicati, non hai considerato la varianza dei campioni. I microbenchmarks corretti * devono * includere l'analisi della significatività statistica, altrimenti l'intero risultato potrebbe essere un colpo di fortuna. Ripetere le prove poche volte non è abbastanza. Inoltre, per ottenere una risposta autorevole qui dovremmo esaminare il codice generato (non IL ma il codice macchina generato dal JIT), che ci mostrerebbe tutto ciò che dobbiamo sapere. Il problema è che il JIT x86 in realtà non esegue molto l'inlining, quindi i risultati sono plausibili. (Su un x64 i risultati dovrebbero essere diversi.) –

+1

Questo non è ciò che intendo per varianza. Intendo [varianza] (https://en.wikipedia.org/wiki/Variance) in senso stretto nelle statistiche e in particolare nel contesto di [test di significatività statistica] (https://en.wikipedia.org/wiki/Significance_testing). Nota, continuo a pensare che questa sia una buona risposta, quindi +1. –

+0

@nawfal Possiamo supporre che la negazione di tutti questi sia la stessa ('(oggetto) p1! = (Oggetto) p2')? Penserei di no, quindi potrebbe essere bello eseguire i test anche su quelli ... :) – christo8989

3

Aggiungendo i miei due centesimi, dopo molte ore di ritardo in un codice critico, su basi di codice molto grandi con a volte profondità di chiamate profonde a volte pazzesche.

Al di fuori di un "micro benchmark" nel mondo reale, il JIT ha molti più problemi e preoccupazioni, e niether ha il lusso del tempo di compilazione C++ WPO, né la facilità dei compilatori C# più traduzioni dirette, eppure tutti i problemi di non avere necessariamente tutto il contesto dopo il compilatore C# sono fatti.

Le forme 'pedante':

if ((object)a == (object)b) { }  // ref equals 

if (!((object)a == (object)b)) { } // ref not equals 

Se avete davvero onesto-a-dio perf problemi pesati e stramazzo, o necessità di prendere la pressione al largo della JIT per alcuni molto grandi classi pervasivi, questo può aiutare un sacco. Lo stesso vale per NullOrEmpty vs '(object) str == null || str.Length == 0 '.

Non avendo il lusso di WPO e tutti i contorni di non sapere in molti casi, quali assiemi possono essere caricati o scaricati dopo essere stati colpiti da JITing, accadono strane cose non deterministiche rispetto a ciò che viene ottimizzato e Come.

Questo è un argomento enorme, ma qui ci sono alcuni punti:

  1. Il JIT inseguirà inlining e registrare optimzation una profondità chiamata verso il basso soltanto finora, e totalmente dipende da ciò che sta andando acceso al momento. Se si finisce per compilare una funzione nella catena una volta a causa dell'uso, e una più in basso nella catena di una corsa diversa, è possibile ottenere risultati drasticamente differnet. La cosa peggiore per molte applicazioni che sono vincolate alla latenza o all'interfaccia utente è il non-depretismo, e nelle app più grandi questo può aumentare rapidamente.

  2. ! ((Oggetto) a == (oggetto) b) e (oggetto) a! = (Oggetto) b non sono sempre compilati verso lo stesso codice, come è vero per certianly! (A = = b) e a! = b, anche senza operatori espliciti o sostituzioni uguali. A '(oggetto) a! = (Oggetto) b' ha molte più probabilità di innescare la propria chiamata più pedante di .Net nel runtime, che è molto costosa.

  3. guardia presto e spesso con '(oggetto)' o '' RefEquals se molto utile per il JIT, anche se attualmente non avete dell'operatore o uguale sostituzioni. (oggetto) è ancora meglio. Se pensi a cosa deve fare il JIT per deteminare se un tipo potrebbe avere delle sostituzioni, e gestire le regole di uguaglianza (e) bizantine (e), non è come un mini inferno, e tutto ciò che potrebbe essere reso pubblico più tardi e intendi ref l'uguaglianza ti salva da un improvviso rallentamento o codice stupido in seguito. Se è già pubblico e non è sigillato, il JIT può dire che le regole avranno o avranno il tempo di inseguirle.

  4. guardia del genere più pervasivi controlli 'Null' è esaltante ancora più importante, anche se non una parte della domanda del PO, come le stesse regole e le questioni si applicano in generale. '(oggetto) a == null' e '! ((oggetto) a == null)' sono gli equivalenti 'pedanti'.

+1

+1 I tuoi punti sono intriganti. Hai scritto questo in dettaglio in un'altra domanda o nel post del blog? Mi piacerebbe sapere di più su come sei arrivato a queste conclusioni. –