2011-08-24 10 views
16

Non chiedermi come ci sono arrivato, ma giocavo con un po 'di mascheramento, srotolamento di loop ecc. In ogni caso, per interesse stavo pensando a come avrei implementato un metodo indexof, e per farla breve, tutto ciò mascherando ecc., questa ingenua implementazione:Perché il mio string.indexof (char) è più veloce?

public static unsafe int IndexOf16(string s, int startIndex, char c) { 
      if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
      fixed (char* cs = s) { 
       for (int i = startIndex; i < s.Length; i++) { 
        if ((cs[i]) == c) return i; 
       } 
       return -1; 
      } 
     } 

è più veloce di string.IndexOf (char). Ho scritto alcuni test semplici e sembra che corrisponda esattamente all'output. Alcuni numeri di output di esempio dalla mia macchina (che varia in una certa misura, naturalmente, ma la tendenza è chiara):

short haystack 500k runs 
1741 ms for IndexOf16 
2737 ms for IndexOf32 
2963 ms for IndexOf64 
2337 ms for string.IndexOf <-- buildin 

longer haystack: 
2888 ms for IndexOf16 
3028 ms for IndexOf32 
2816 ms for IndexOf64 
3353 ms for string.IndexOf <-- buildin 

IndexOfChar è contrassegnato extern, quindi potete riflettore esso. Comunque penso che questa dovrebbe essere l'implementazione (nativa): http://www.koders.com/cpp/fidAB4768BA4DF45482A7A2AA6F39DE9C272B25B8FE.aspx?s=IndexOfChar#L1000

Sembra che usino la stessa implementazione ingenua.

domande mi vengono in mente:

1) mi sto perdendo qualcosa nella mia implementazione che spiega il motivo per cui il suo più veloce? Posso solo pensare al supporto esteso dei caratteri, ma la loro implementazione suggerisce che non fanno nulla di speciale neanche per quello.

2) Ho pensato che molti dei metodi di basso livello sarebbero stati implementati in assembler manuale, il che non sembra essere il caso. In tal caso, perché implementarlo in modo nativo, invece di solo in C# come la mia implementazione di esempio?

(Test completo qui (penso che sia troppo lungo per incollare qui): http://paste2.org/p/1606018)

(No questo non è l'ottimizzazione prematura, non è per un progetto che sto solo prendendo in giro circa) :-)

Aggiornamento: da Thnx a Oliver per l'indicazione su nullcheck e il parametro Count. Ho aggiunto questi al mio IndexOf16Implementation in questo modo:

public static unsafe int IndexOf16(string s, int startIndex, char c, int count = -1) { 
    if (s == null) throw new ArgumentNullException("s"); 
    if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
    if (count == -1) count = s.Length - startIndex; 
    if (count < 0 || count > s.Length - startIndex) throw new ArgumentOutOfRangeException("count"); 

    int endIndex = startIndex + count; 
    fixed (char* cs = s) { 
     for (int i = startIndex; i < endIndex; i++) { 
      if ((cs[i]) == c) return i; 
     } 
     return -1; 
    } 
} 

I numeri cambiato un po ', ma è ancora abbastanza significativamente più veloce (32/64 risultati omessi):

short haystack 500k runs 
1908 ms for IndexOf16 
2361 ms for string.IndexOf 
longer haystack: 
3061 ms for IndexOf16 
3391 ms for string.IndexOf 

Update2: questa versione è più veloce ma (soprattutto per il lungo caso pagliaio):

public static unsafe int IndexOf16(string s, int startIndex, char c, int count = -1) { 
      if (s == null) throw new ArgumentNullException("s"); 
      if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
      if (count == -1) count = s.Length - startIndex; 
      if (count < 0 || count > s.Length - startIndex) throw new ArgumentOutOfRangeException("count"); 

      int endIndex = startIndex + count; 
      fixed (char* cs = s) { 
       char* cp = cs + startIndex; 
       for (int i = startIndex; i <= endIndex; i++, cp++) { 
        if (*cp == c) return i; 
       } 
       return -1; 
      } 
     } 

Update 4: Sulla base della discussione con LastCoder credo che dipenda dall'architettura. Il mio Xeon W3550 alle opere sembra preferire questa versione, mentre il suo i7 sembra apprezzare la versione buildin. La mia macchina domestica (Athlon II) sembra essere nel mezzo. Sono sorpreso dalla grande differenza però.

+0

mask1 deve essere 0xffff anziché 0xff – hazzik

+0

@hazzik thnx per il suggerimento – chrisaut

risposta

4

Possibilità 1) Questo non può tenere (come vero) in C#, ma quando ho fatto il lavoro di ottimizzazione per x86-64 assembler Ho subito scoperto, mentre l'analisi comparativa che chiamare il codice da una DLL (contrassegnato esterno) è stato più lento di implementare la stessa funzione esatta all'interno del mio eseguibile. Il motivo più ovvio è il paging e la memoria, il metodo DLL (esterno) viene caricato molto lontano nella memoria dal resto del codice in esecuzione e, se non è stato effettuato l'accesso in precedenza, sarà necessario effettuare il paging. Il codice di benchmarking dovrebbe fare alcuni cicli di riscaldamento delle funzioni di cui si sta eseguendo il benchmarking per assicurarsi che vengano cercati in memoria prima di cronometrarli.

Possibilità 2) Microsoft tende a non ottimizzare le funzioni di stringa al massimo, in modo da ottimizzare una lunghezza di stringa nativa, sottostringa, indexof, ecc. Non è davvero inaudita. aneddoto; nell'assembler x86-64 sono stato in grado di creare una versione della funzione RtlInitUnicodeString di WinXP64 che è stata eseguita 2x volte più velocemente in quasi tutti i casi di utilizzo pratico.

Possibilità 3) Il codice di benchmarking mostra che si sta utilizzando il sovraccarico di 2 parametri per IndexOf, questa funzione probabilmente chiama il 3 parametri di sovraccarico IndexOf (Char, Int32, Int32) che aggiunge un overhead in più per ogni iterazione.


Questo può essere ancora più veloce perché si rimuove l'incremento di variabile i per iterazione.

  char* cp = cs + startIndex; 
      char* cpEnd = cp + endIndex; 
      while (cp <= cpEnd) { 
       if (*cp == c) return cp - cs; 
       cp++; 
      } 

modificare In risposta riguardo (2) per la vostra curiosità, codificato nel 2005 e utilizzato per patchare il ntdll.dll della mia macchina WinXP64. http://board.flatassembler.net/topic.php?t=4467

RtlInitUnicodeString_Opt: ;;rcx=buff rdx=ucharstr 77bytes 
      xor r9d,r9d 
      test rdx,rdx 
      mov dword[rcx],r9d 
      mov [rcx+8],rdx 
      jz  .end 
      mov r8,rdx 
    .scan: 
      mov eax,dword[rdx] 

      test ax,ax 
      jz  .one 
      add rdx,4 
      shr eax,16 
      test ax,ax 
      jz  .two 
      jmp .scan 
    .two: 
      add rdx,2 
    .one: 
      mov eax,0fffch 
      sub rdx,r8 
      cmp rdx,0fffeh 
      cmovnb rdx,rax 
      mov [ecx],dx 
      add dx,2 
      mov [ecx+2],dx 
      ret 
    .end: 
      retn 

Edit 2 Il funzionamento del vostro codice di esempio (aggiornato con la versione più veloce) lo String.IndexOf corre più veloce sul mio i7 di Intel, 4 GB di RAM, Win7 64 bit.

short haystack 500k runs 
2590 ms for IndexOf16 
2287 ms for string.IndexOf 
longer haystack: 
3549 ms for IndexOf16 
2757 ms for string.IndexOf 

Le ottimizzazioni a volte sono molto legate all'architettura.

+0

Thnx. 1) il codice dovrebbe essere completo (jitted e indovinato) nel momento in cui viene eseguito il benchmark, perché è stato testato prima per correttezza. Ma buon punto. 2) Mi piacerebbe vedere che il codice :-) – chrisaut

+0

3) aiuta a quella inbuild un po ', ma non abbastanza: breve 500k pagliaio corre 1669 ms per IndexOf16 2319 ms per String.IndexOf con 2 2094 ms per String.IndexOf con 3 più pagliaio: 2289 ms per IndexOf16 3238 ms per String.IndexOf con 2 3153 ms per String.IndexOf con 3 – chrisaut

+0

per quanto riguarda la versione, ho avuto quasi la stessa cosa. :-) Comunque hai provato il tuo (hai dimenticato un cast e il ritorno -1). È più lento: ~ 2150/~ 2700 per prova breve/lunga Bel suggerimento, però, il numero – chrisaut

2

Se si effettua una misura di micro misurazione, ogni singolo bit conta. Nell'implementazione della MS (come si vede nel link che hai fornito) controllano anche se s è nullo e lancia una NullArgumentException. Anche questa è l'implementazione incluso il parametro count. Quindi controllano anche se contano come valore corretto e lanciano ArgumentOutOfRangeException.

Penso che questi piccoli controlli per rendere il codice più robusto siano sufficienti a renderli un po 'più lenti se li si chiama così spesso in così poco tempo.

+0

Thnx per il suggerimento e il buon punto. Ho aggiunto il controllo null della stringa e un'implementazione di conteggio. Non cambia in modo significativo i numeri. Vedi la mia domanda aggiornata. – chrisaut

1

Questo potrebbe avere qualcosa a che fare con l'istruzione "fissa" come "Appunta la posizione degli oggetti src e dst in memoria in modo che non vengano spostati dalla garbage collection." forse accelerando i metodi?

Anche "Il codice non sicuro aumenta le prestazioni eliminando i controlli sui limiti di array." questo potrebbe anche essere il motivo.

osservazioni di cui sopra presi dalla MSDN

+0

Se qualcosa è stato risolto dovrebbe renderlo più lento rispetto all'implementazione nativa in quanto probabilmente hanno altri modi per comunicare con gc. E naturalmente dal momento che sono nativi, il controllo dei limiti non dovrebbe avere un ruolo. – chrisaut

+0

@Steven, la risposta potrebbe non essere "Diversamente dall'assegnazione di array dinamici, una matrice di lunghezza fissa verrà considerata come parte della struttura anziché solo un riferimento, ma ha anche il vantaggio di essere un tipo non gestito e quindi una struttura che lo usa sarà stack assegnato per impostazione predefinita. "Quindi, poiché il codice non gestito è in esecuzione nello stack, è più veloce di un codice gestito che potrebbe essere eseguito su heap? – Jethro

+0

Non sono abbastanza sicuro di cosa intendi. Non sto assegnando alcun array, vero? Cosa intendi per "correre in pila". Lo stack AFAIK vs l'heap ha solo un ruolo nell'assegnazione dei mem.Suppongo che nessuna implementazione faccia alcuna allocazione dell'heap. Mi sto perdendo qualcosa? – chrisaut

Problemi correlati