2013-07-10 7 views
13

Il String.Contains metodo assomiglia a questo internamentePerché String.Contain non chiama direttamente l'overload finale?

public bool Contains(string value) 
{ 
    return this.IndexOf(value, StringComparison.Ordinal) >= 0; 
} 

Il IndexOf di sovraccarico che si chiama aspetto come questo

public int IndexOf(string value, StringComparison comparisonType) 
{ 
    return this.IndexOf(value, 0, this.Length, comparisonType); 
} 

Ecco un'altra chiamata è fatto per il sovraccarico di finale, che poi chiama il metodo in questione CompareInfo.IndexOf, con la firma

public int IndexOf(string value, int startIndex, int count, StringComparison comparisonType) 

Pertanto, chiamando il il sovraccarico finale sarebbe il più veloce (sebbene possa essere considerato un micro-ottimizzazione nella maggior parte dei casi).

È possibile che manchi qualcosa di ovvio, ma perché il metodo Contains non chiama il sovraccarico finale considerando direttamente che nessun altro lavoro è svolto nella chiamata intermedia e che le stesse informazioni sono disponibili in entrambe le fasi?

L'unico vantaggio è che se la firma del sovraccarico finale cambia, è necessario effettuare una sola modifica (quella del metodo intermedio) oppure esiste più rispetto alla progettazione?

Edit dai commenti (vedere Update 2 per la differenza di velocità spiegazione)

Per chiarire le differenze di prestazioni che sto ricevendo nel caso in cui ho fatto un errore da qualche parte: mi sono imbattuto this benchmark (loop 5 volte per evitare distorsioni jitter) e usato questo metodo di estensione da confrontare con il metodo String.Contains

public static bool QuickContains(this string input, string value) 
{ 
    return input.IndexOf(value, 0, input.Length, StringComparison.OrdinalIgnoreCase) >= 0; 
} 

con il ciclo simile a questo

for (int i = 0; i < 1000000; i++) 
{ 
    bool containsStringRegEx = testString.QuickContains("STRING"); 
} 
sw.Stop(); 
Console.WriteLine("QuickContains: " + sw.ElapsedMilliseconds); 

Nel test di benchmark, QuickContains sembra circa il 50% più veloce di String.Contains sulla mia macchina.

Aggiornamento 2 (differenza di prestazioni spiegato)

ho notato qualcosa di sleale nel benchmark che spiega molte cose. Lo stesso benchmark era per misurare stringhe maiuscole e minuscole, ma dal momento che String.Contains è in grado di eseguire solo ricerche sensibili al maiuscolo/minuscolo, è stato incluso il metodo ToUpper. Ciò potrebbe distorcere i risultati, non in termini di output finale, ma almeno in termini di semplice misurazione delle prestazioni di String.Contains in ricerche non sensibili al maiuscolo/minuscolo.

Così ora, se io uso questo metodo di estensione

public static bool QuickContains(this string input, string value) 
{ 
    return input.IndexOf(value, 0, input.Length, StringComparison.Ordinal) >= 0; 
} 

uso StringComparison.Ordinal nel 2 di sovraccarico IndexOf chiamata e rimuovere ToUpper, il metodo QuickContains diventa in realtà la più lenta. IndexOf e Contains sono praticamente alla pari in termini di prestazioni. Quindi chiaramente era la chiamata ToUpper che alterava i risultati del perché c'era una tale discrepanza tra Contains e IndexOf.

Non so perché il metodo di estensione QuickContains è diventato il più lento.(Probabilmente correlato al fatto che lo Contains ha l'attributo [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]?).

Domanda rimane ancora il motivo per cui il metodo di sovraccarico a 4 non viene chiamato direttamente ma sembra che le prestazioni non siano influenzate (come Adrian e Delnan hanno sottolineato nei commenti) dalla decisione.

+0

E 'possibile che la chiamata al metodo sarà inline dal compilatore, in modo che qualsiasi le occorrenze di 'String.Contains (valore stringa)' possono già essere riscritte nella versione più complessa al momento in cui il codice viene eseguito (lasciando l'unico motivo essere il vantaggio che si menziona, senza svantaggi). Tuttavia, questa è ovviamente solo una congettura. –

+0

Questa è una buona idea. Ho appena provato il benchmarking creando un metodo personalizzato 'Contains' che chiama direttamente il sovraccarico finale e ha finito per essere il 50% più veloce del metodo' Contains' esistente, quindi c'è un notevole riscontro di prestazioni (anche se piccolo in termini assoluti; 200ms oltre 1m di loop). – keyboardP

+0

@keyboardP Trovo che sia un po 'difficile da credere; anche la più semplice euristica in linea e il più semplice inliner dovrebbero rendere le due varianti indistinguibili. Sei sicuro di non aver commesso [errori di benchmarking] (http://tech.pro/blog/1293/c-performance-benchmark-mistakes-part-one)? – delnan

risposta

5

Sono passato un po 'di tempo da quando ho guardato il montaggio, e so quasi nulla di MSIL e JIT, quindi sarebbe un bel esercizio - non ho potuto resistere, quindi ecco un po' di, forse dati empirici ridondanti. Il sovraccarico di IndexOf si inline?

Ecco una piccola console app:

class Program 
{ 
    static void Main(string[] args) 
    { 
     "hello".Contains("hell"); 
    } 
} 

Il JIT genera questo in una build di rilascio ottimizzato, qualsiasi CPU, in esecuzione in 32 bit. Ho abbreviato gli indirizzi, e rimosso alcune linee irrilevanti:

--- ...\Program.cs 
      "hello".Contains("hell"); 
[snip] 
17 mov   ecx,dword ptr ds:[0320219Ch] ; pointer to "hello" 
1d mov   edx,dword ptr ds:[032021A0h] ; pointer to "hell" 
23 cmp   dword ptr [ecx],ecx 
25 call  680A6A6C      ; String.Contains() 
[snip] 

Il call a 0x00000025 va qui:

String.Contains

00 push  0     ; startIndex = 0 
02 push  dword ptr [ecx+4] ; count = this.Length (second DWORD of String) 
05 push  4     ; comparisonType = StringComparison.Ordinal 
07 call  FF9655A4   ; String.IndexOf() 
0c test  eax,eax 
0e setge  al    ; if (... >= 0) 
11 movzx  eax,al 
14 ret 

Abbastanza sicuro, sembra chiamare , direttamente, l'ultimo String.IndexOf sovraccarico con quattro argomenti: tre push ed; uno in edx (value: "inferno"); this ("ciao") in ecx. Per confermare, questo è dove il call a 0x00000005 va:

00 push  ebp 
01 mov   ebp,esp 
03 push  edi 
04 push  esi 
05 push  ebx 
06 mov   esi,ecx     ; this ("hello") 
08 mov   edi,edx     ; value ("hell") 
0a mov   ebx,dword ptr [ebp+10h] 
0d test  edi,edi     ; if (value == null) 
0f je   00A374D0 
15 test  ebx,ebx     ; if (startIndex < 0) 
17 jl   00A374FB 
1d cmp   dword ptr [esi+4],ebx ; if (startIndex > this.Length) 
20 jl   00A374FB 
26 cmp   dword ptr [ebp+0Ch],0 ; if (count < 0) 
2a jl   00A3753F 
[snip] 

... che sarebbe il corpo:

public int IndexOf(string value, 
        int startIndex, 
        int count, 
        StringComparison comparisonType) 
{ 
    if (value == null) 
    throw new ArgumentNullException("value"); 
    if (startIndex < 0 || startIndex > this.Length) 
    throw new ArgumentOutOfRangeException("startIndex", 
      Environment.GetResourceString("ArgumentOutOfRange_Index")); 
    if (count < 0 || startIndex > this.Length - count) 
    throw new ArgumentOutOfRangeException("count", 
      Environment.GetResourceString("ArgumentOutOfRange_Count")); 
    ... 
} 
+0

Wow, grazie per lo sforzo! Sembra che la differenza di prestazioni sia trascurabile (per le ricerche non sensibili al maiuscolo/minuscolo) e quindi si tratta di una decisione di progettazione. Immagino che potrebbe davvero essere solo il caso di dover evitare due modifiche al codice se la firma del sovraccarico 4 dovesse cambiare (comunque improbabile). – keyboardP

+0

Ovviamente, ora mi sono reso conto che lo stack di chiamate poteva dirmi questo. Ma divertente avere il mio primo vero aspetto nell'output JIT, comunque. – JimmiTh

+0

Non ho approfondito l'assemblea in modo reale, quindi la tua risposta è stata molto interessante da seguire. – keyboardP

Problemi correlati