2011-08-24 33 views
16

No preguntes cómo llegué allí, pero estaba jugando con algo de enmascaramiento, desenrollado de lazo, etc. En cualquier caso, por interés, estaba pensando cómo implementaría un índice de método, y para resumir, todo eso enmascaramiento, etc. a un lado, esta implementación ingenua:¿Por qué mi string.indexof (char) es más rápido?

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

es más rápido que string.IndexOf (char). Escribí algunas pruebas simples, y parece coincidir exactamente con la salida. Algunos números de salida de muestra de mi máquina (que varía en cierto grado, por supuesto, pero la tendencia es clara):

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 está marcado externo, por lo que no puede reflector de ella. Sin embargo, creo que esta debería ser la implementación (nativa): http://www.koders.com/cpp/fidAB4768BA4DF45482A7A2AA6F39DE9C272B25B8FE.aspx?s=IndexOfChar#L1000

Parece que usan la misma implementación ingenua.

Las preguntas vienen a la mente:

1) ¿Me estoy perdiendo algo en mi aplicación que explica por qué es más rápido? Solo puedo pensar en el soporte de caracteres extendidos, pero su implementación sugiere que tampoco hacen nada especial para eso.

2) Supuse que gran parte de los métodos de bajo nivel finalmente se implementarían en el ensamblador manual, eso no parece ser el caso. Si es así, ¿por qué implementarlo de forma nativa en absoluto, en lugar de simplemente en C# como mi implementación de muestra?

(prueba completa aquí (creo que es demasiado largo para pegar aquí): http://paste2.org/p/1606018)

(No, esto no es la optimización prematura, no es para un proyecto que estoy solo Juguetón) :-)

Update: Thnx a Oliver por la pista sobre nullcheck y Count param. He añadido estas a mi IndexOf16Implementation así:

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

Los números cambian ligeramente, sin embargo, es todavía bastante significativamente más rápido (32/64 resultados omitidos):

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: Esta versión sin embargo, es más rápido (sobre todo para el caso pajar largo):

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

actualización 4: Basado en la discusión con LastCoder, creo que esto depende de la arquitectura. Mi Xeon W3550 en las obras parece preferir esta versión, mientras que a su i7 parece gustarle la versión buildin. Mi máquina doméstica (Athlon II) parece estar en el medio. Sin embargo, estoy sorprendido por la gran diferencia.

+0

mask1 debe ser 0xffff en lugar de 0xff – hazzik

+0

@hazzik thnx para la sugerencia – chrisaut

Respuesta

4

Posibilidad 1) Esto no puede sostener (como verdadero) en C# pero cuando lo hice trabajo de optimización para x86-64 ensamblador que rápidamente se enteraron, mientras que la evaluación comparativa de código de llamada desde un archivo DLL (marcado externo) fue más lento que implementar la misma función exacta dentro de mi ejecutable. La razón más obvia es la paginación y la memoria, el método DLL (externo) se carga muy lejos en la memoria del resto del código en ejecución y, si no se accedió previamente, tendrá que ser localizado. Su código de referencia debería hacer algunos ciclos de calentamiento de las funciones que está evaluando para asegurarse de que son paginados en la memoria antes de cronometrarlos.

Posibilidad 2) Microsoft tiende a no optimizar las funciones de cadena al máximo, por lo que optimizar la longitud de una cuerda nativa, subcadena, índice de etc. no es algo realmente extraño. Anécdota; en el ensamblador x86-64 pude crear una versión de la función RtlInitUnicodeString de WinXP64 que se ejecutó 2 veces más rápido en casi todos los casos prácticos.

Posibilidad 3) Su código de benchmarking muestra que está utilizando la sobrecarga de 2 parámetros para IndexOf, esta función probablemente invoca a los 3 parámetros sobrecarga IndexOf (Char, Int32, Int32) que agrega una sobrecarga adicional a cada iteración.


Esto puede ser incluso más rápido porque elimina el incremento de la variable i por iteración.

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

edición En respuesta con respecto a (2) para su curiosidad, codificado en 2005 y utilizado para parchear el ntdll.dll de mi máquina 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 

edición 2 El funcionamiento de su código de ejemplo (actualizado con la versión más rápida) el String.indexOf corre más rápido en mi Intel i7, 4 GB de RAM, Win7 de 64 bits.

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

Las optimizaciones a veces dependen de la arquitectura.

+0

Thnx. 1) el código debe estar completamente (jodido y creo que paginado) en el momento en que se compara, porque primero se ha probado su corrección. Pero buen punto. 2) Me gustaría ver que el código :-) – chrisaut

+0

3) ayuda a la inbuild ligeramente, pero no lo suficiente: corta 500k pajar corre 1669 ms para IndexOf16 2319 ms para String.indexOf con 2 2094 ms para string.IndexOf con 3 Haystack más largo: 2289 ms para IndexOf16 3238 ms para string.IndexOf con 2 3153 ms para string.IndexOf con 3 – chrisaut

+0

En cuanto a su versión, tenía casi exactamente lo mismo. :-) Sin embargo, intentó el suyo (se olvidó de un elenco y el retorno -1). Es más lento: ~ 2150/~ 2700 para prueba corta/larga Buena sugerencia, thnx – chrisaut

2

Si realmente hace una comprobación de micro medición, cada bit cuenta. Dentro de la implementación de MS (como se ve en el enlace que proporcionó), también verifican si s es nulo y lanzan una NullArgumentException. También esta es la implementación que incluye el parámetro de recuento. Por lo tanto, también verifican si cuentan como un valor correcto y lanzan una ArgumentOutOfRangeException.

Creo que estas pequeñas comprobaciones para que el código sea más robusto son suficientes para hacerlas un poco más lentas si las llamas con tanta frecuencia en tan poco tiempo.

+0

Thnx para la sugerencia y punto bueno. He agregado una verificación de cadena nula y una implementación de recuento. No cambia significativamente los números. Ver mi pregunta actualizada – chrisaut

1

Esto podría tener algo que ver con la declaración "fija" como "Fija la ubicación de los objetos src y dst en la memoria para que no se muevan por la recolección de elementos no utilizados". quizás acelerando los métodos?

También "Código inseguro aumenta el rendimiento al deshacerse de las verificaciones de límites de la matriz". esto también podría ser el por qué.

los comentarios de arriba, tomada desde MSDN

+0

Si algo se soluciona debería hacerlo más lento en comparación con su implementación nativa ya que probablemente tengan otras formas de comunicarse con el gc. Y, por supuesto, dado que son nativos, la verificación de límites no debe desempeñar un papel. – chrisaut

+0

@Steven, ¿No podría ser la respuesta "A diferencia de la utilización de la asignación de matriz dinámica, una matriz de longitud fija se considerará como parte de la estructura en lugar de solo una referencia. También tiene la ventaja añadida de ser un tipo no administrado y una estructura que lo usará será asignado por pila de forma predeterminada. "Entonces, como su código no administrado se está ejecutando en la pila, es más rápido que un código administrado que podría ejecutarse en el montón? – Jethro

+0

No estoy muy seguro de lo que quieres decir. No estoy asignando matrices, ¿verdad? ¿Qué quieres decir con "correr en la pila"? AFAIK stack vs heap solo juega un papel en la asignación de mem.Supongo que ninguna implementación realiza asignaciones de montón. ¿Me estoy perdiendo de algo? – chrisaut

Cuestiones relacionadas