2012-06-28 23 views
22

He escrito dos funciones que convierten una cadena de enteros separados por espacios en blanco en una matriz int. La primera función utiliza Substring y luego se aplica System.Int32.Parse para convertir la subcadena en un valor int:Análisis más rápido de números en .NET

let intsOfString (s: string) = 
    let ints = ResizeArray() 
    let rec inside i j = 
    if j = s.Length then 
     ints.Add(s.Substring(i, j-i) |> System.Int32.Parse) 
    else 
     let c = s.[j] 
     if '0' <= c && c <= '9' then 
     inside i (j+1) 
     else 
     ints.Add(s.Substring(i, j-i) |> System.Int32.Parse) 
     outside (j+1) 
    and outside i = 
    if i < s.Length then 
     let c = s.[i] 
     if '0' <= c && c <= '9' then 
     inside i (i+1) 
     else 
     outside (i+1) 
    outside 0 
    ints.ToArray() 

La segunda función atraviesa los caracteres de la cadena en el lugar acumular el número entero sin crear una subcadena temporal:

let intsOfString (s: string) = 
    let ints = ResizeArray() 
    let rec inside n i = 
    if i = s.Length then 
     ints.Add n 
    else 
     let c = s.[i] 
     if '0' <= c && c <= '9' then 
     inside (10*n + int c - 48) (i+1) 
     else 
     ints.Add n 
     outside(i+1) 
    and outside i = 
    if i < s.Length then 
     let c = s.[i] 
     if '0' <= c && c <= '9' then 
     inside (int c - 48) (i+1) 
     else 
     outside (i+1) 
    outside 0 
    ints.ToArray() 

Benchmarking en enteros separados por espacios 1 a 1,000,000, la primera versión toma 1.5s mientras que la segunda versión toma 0.3s.

Analizar tales valores puede ser crítico para el rendimiento, por lo que dejar el rendimiento 5x en la tabla mediante el uso de subcadenas temporales puede ser indeseable. El análisis de números enteros es fácil, pero el análisis de otros valores como números de punto flotante, decimales y fechas es considerablemente más difícil.

Entonces, ¿hay funciones integradas para analizar directamente desde una subcadena dentro de una cadena (es decir, utilizando el inicio y la longitud de una cadena) para evitar generar una cadena temporal? Si no, ¿hay alguna biblioteca que brinde funciones eficientes para hacer esto?

+4

¿Ha intentado utilizar expresiones regulares en lugar de utilizar Substring? Una expresión regular compilada puede ser mucho más rápida que las operaciones de cadena –

+3

@PanagiotisKanavos ¿Puede explicar cómo se puede usar una expresión regular para analizar una cadena en una matriz de entradas? –

+1

Tuve un problema similar recientemente y no pude encontrar ninguno cuando busqué, tuve que escribir el código de análisis decimal yo mismo. No es tan difícil como podrías pensar, ya que la clase Decimal tiene un constructor que toma un factor de escala para que puedas hacer más o menos lo mismo que el análisis de enteros y simplemente hacer un seguimiento de dónde está el punto decimal. Las fechas tampoco fueron muy difíciles, sin embargo tuve un control estricto sobre los formatos en ambos casos. No me gustaría escribir el código de análisis general ... – MrKWatkins

Respuesta

0

No estoy seguro si esto es bueno, pero ¿ha intentado algo así como:

var stringValues = input.split(" "); 

var intValues = Array.ConvertAll(stringValues, s => int.Parse(s)); 
+0

Esto crea cadenas temporales; OP no quería esto ... – MrKWatkins

+3

Sí, eso es incluso más lento que la versión lenta porque no solo está asignando cadenas temporales sino también manteniendo referencias a ellas, por lo que está pagando por las barreras de escritura y el costo de la evacuación de GC ellos de la generación de vivero. –

+0

La pregunta parece haber cambiado para incluir una implementación F # ... ¿Alguien quiere traducir la respuesta? – Paddy

5

he escrito éste en dobles, que no crea una subcadena temporal. Está destinado a ser utilizado dentro de un analizador JSON, por lo que se limita a cómo se pueden representar los dobles en JSON de acuerdo con http://www.json.org/.

Aún no es óptimo porque requiere que sepa dónde comienza y termina el número (begin y end parámetros), por lo que tendrá que recorrer la longitud del número dos veces para averiguar dónde termina. Sigue siendo alrededor de 10-15 veces más rápido que double.Parse y podría modificarse con bastante facilidad si encuentra el end dentro de la función que luego se devuelve como un parámetro out para saber dónde debe reanudar el análisis de la cadena principal.

usados ​​de esta manera: Código

Parsers.TryParseDoubleFastStream("1", 0, 1, out j); 
Parsers.TryParseDoubleFastStream("2.0", 0, 3, out j); 
Parsers.TryParseDoubleFastStream("3.5", 0, 3, out j); 
Parsers.TryParseDoubleFastStream("-4.5", 0, 4, out j); 
Parsers.TryParseDoubleFastStream("50.06", 0, 5, out j); 
Parsers.TryParseDoubleFastStream("1000.65", 0, 7, out j); 
Parsers.TryParseDoubleFastStream("-10000.8600", 0, 11, out j); 

se puede encontrar aquí:

https://gist.github.com/3010984 (sería demasiado largo para publicar aquí).

Y StandardFunctions.IgnoreChar es para mi propósito tan simple como:

public static bool IgnoreChar(char c) 
{ 
    return c < 33; 
} 
8

System.Int32.Parse es slowlest, porque utilizó CultureInfo, FormatInfo y etc; y la razón de rendimiento no está en las cadenas temporales.

Código de la reflexión:

private unsafe static bool ParseNumber(ref char* str, NumberStyles options, ref Number.NumberBuffer number, NumberFormatInfo numfmt, bool parseDecimal) 
{ 
    number.scale = 0; 
    number.sign = false; 
    string text = null; 
    string text2 = null; 
    string str2 = null; 
    string str3 = null; 
    bool flag = false; 
    string str4; 
    string str5; 
    if ((options & NumberStyles.AllowCurrencySymbol) != NumberStyles.None) 
    { 
     text = numfmt.CurrencySymbol; 
     if (numfmt.ansiCurrencySymbol != null) 
     { 
      text2 = numfmt.ansiCurrencySymbol; 
     } 
     str2 = numfmt.NumberDecimalSeparator; 
     str3 = numfmt.NumberGroupSeparator; 
     str4 = numfmt.CurrencyDecimalSeparator; 
     str5 = numfmt.CurrencyGroupSeparator; 
     flag = true; 
    } 
    else 
    { 
     str4 = numfmt.NumberDecimalSeparator; 
     str5 = numfmt.NumberGroupSeparator; 
    } 
    int num = 0; 
    char* ptr = str; 
    char c = *ptr; 
    while (true) 
    { 
     if (!Number.IsWhite(c) || (options & NumberStyles.AllowLeadingWhite) == NumberStyles.None || ((num & 1) != 0 && ((num & 1) == 0 || ((num & 32) == 0 && numfmt.numberNegativePattern != 2)))) 
     { 
      bool flag2; 
      char* ptr2; 
      if ((flag2 = (((options & NumberStyles.AllowLeadingSign) == NumberStyles.None) ? false : ((num & 1) == 0))) && (ptr2 = Number.MatchChars(ptr, numfmt.positiveSign)) != null) 
      { 
       num |= 1; 
       ptr = ptr2 - (IntPtr)2/2; 
      } 
      else 
      { 
       if (flag2 && (ptr2 = Number.MatchChars(ptr, numfmt.negativeSign)) != null) 
       { 
        num |= 1; 
        number.sign = true; 
        ptr = ptr2 - (IntPtr)2/2; 
       } 
       else 
       { 
        if (c == '(' && (options & NumberStyles.AllowParentheses) != NumberStyles.None && (num & 1) == 0) 
        { 
         num |= 3; 
         number.sign = true; 
        } 
        else 
        { 
         if ((text == null || (ptr2 = Number.MatchChars(ptr, text)) == null) && (text2 == null || (ptr2 = Number.MatchChars(ptr, text2)) == null)) 
         { 
          break; 
         } 
         num |= 32; 
         text = null; 
         text2 = null; 
         ptr = ptr2 - (IntPtr)2/2; 
        } 
       } 
      } 
     } 
     c = *(ptr += (IntPtr)2/2); 
    } 
    int num2 = 0; 
    int num3 = 0; 
    while (true) 
    { 
     if ((c >= '0' && c <= '9') || ((options & NumberStyles.AllowHexSpecifier) != NumberStyles.None && ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))) 
     { 
      num |= 4; 
      if (c != '0' || (num & 8) != 0) 
      { 
       if (num2 < 50) 
       { 
        number.digits[(IntPtr)(num2++)] = c; 
        if (c != '0' || parseDecimal) 
        { 
         num3 = num2; 
        } 
       } 
       if ((num & 16) == 0) 
       { 
        number.scale++; 
       } 
       num |= 8; 
      } 
      else 
      { 
       if ((num & 16) != 0) 
       { 
        number.scale--; 
       } 
      } 
     } 
     else 
     { 
      char* ptr2; 
      if ((options & NumberStyles.AllowDecimalPoint) != NumberStyles.None && (num & 16) == 0 && ((ptr2 = Number.MatchChars(ptr, str4)) != null || (flag && (num & 32) == 0 && (ptr2 = Number.MatchChars(ptr, str2)) != null))) 
      { 
       num |= 16; 
       ptr = ptr2 - (IntPtr)2/2; 
      } 
      else 
      { 
       if ((options & NumberStyles.AllowThousands) == NumberStyles.None || (num & 4) == 0 || (num & 16) != 0 || ((ptr2 = Number.MatchChars(ptr, str5)) == null && (!flag || (num & 32) != 0 || (ptr2 = Number.MatchChars(ptr, str3)) == null))) 
       { 
        break; 
       } 
       ptr = ptr2 - (IntPtr)2/2; 
      } 
     } 
     c = *(ptr += (IntPtr)2/2); 
    } 
    bool flag3 = false; 
    number.precision = num3; 
    number.digits[(IntPtr)num3] = '\0'; 
    if ((num & 4) != 0) 
    { 
     if ((c == 'E' || c == 'e') && (options & NumberStyles.AllowExponent) != NumberStyles.None) 
     { 
      char* ptr3 = ptr; 
      c = *(ptr += (IntPtr)2/2); 
      char* ptr2; 
      if ((ptr2 = Number.MatchChars(ptr, numfmt.positiveSign)) != null) 
      { 
       c = *(ptr = ptr2); 
      } 
      else 
      { 
       if ((ptr2 = Number.MatchChars(ptr, numfmt.negativeSign)) != null) 
       { 
        c = *(ptr = ptr2); 
        flag3 = true; 
       } 
      } 
      if (c >= '0' && c <= '9') 
      { 
       int num4 = 0; 
       do 
       { 
        num4 = num4 * 10 + (int)(c - '0'); 
        c = *(ptr += (IntPtr)2/2); 
        if (num4 > 1000) 
        { 
         num4 = 9999; 
         while (c >= '0' && c <= '9') 
         { 
          c = *(ptr += (IntPtr)2/2); 
         } 
        } 
       } 
       while (c >= '0' && c <= '9'); 
       if (flag3) 
       { 
        num4 = -num4; 
       } 
       number.scale += num4; 
      } 
      else 
      { 
       ptr = ptr3; 
       c = *ptr; 
      } 
     } 
     while (true) 
     { 
      if (!Number.IsWhite(c) || (options & NumberStyles.AllowTrailingWhite) == NumberStyles.None) 
      { 
       bool flag2; 
       char* ptr2; 
       if ((flag2 = (((options & NumberStyles.AllowTrailingSign) == NumberStyles.None) ? false : ((num & 1) == 0))) && (ptr2 = Number.MatchChars(ptr, numfmt.positiveSign)) != null) 
       { 
        num |= 1; 
        ptr = ptr2 - (IntPtr)2/2; 
       } 
       else 
       { 
        if (flag2 && (ptr2 = Number.MatchChars(ptr, numfmt.negativeSign)) != null) 
        { 
         num |= 1; 
         number.sign = true; 
         ptr = ptr2 - (IntPtr)2/2; 
        } 
        else 
        { 
         if (c == ')' && (num & 2) != 0) 
         { 
          num &= -3; 
         } 
         else 
         { 
          if ((text == null || (ptr2 = Number.MatchChars(ptr, text)) == null) && (text2 == null || (ptr2 = Number.MatchChars(ptr, text2)) == null)) 
          { 
           break; 
          } 
          text = null; 
          text2 = null; 
          ptr = ptr2 - (IntPtr)2/2; 
         } 
        } 
       } 
      } 
      c = *(ptr += (IntPtr)2/2); 
     } 
     if ((num & 2) == 0) 
     { 
      if ((num & 8) == 0) 
      { 
       if (!parseDecimal) 
       { 
        number.scale = 0; 
       } 
       if ((num & 16) == 0) 
       { 
        number.sign = false; 
       } 
      } 
      str = ptr; 
      return true; 
     } 
    } 
    str = ptr; 
    return false; 
} 
public static int Parse(string s) 
{ 
    return Number.ParseInt32(s, NumberStyles.Integer, NumberFormatInfo.CurrentInfo); 
} 

internal unsafe static int ParseInt32(string s, NumberStyles style, NumberFormatInfo info) 
{ 
    byte* stackBuffer = stackalloc byte[1 * 114/1]; 
    Number.NumberBuffer numberBuffer = new Number.NumberBuffer(stackBuffer); 
    int result = 0; 
    Number.StringToNumber(s, style, ref numberBuffer, info, false); 
    if ((style & NumberStyles.AllowHexSpecifier) != NumberStyles.None) 
    { 
     if (!Number.HexNumberToInt32(ref numberBuffer, ref result)) 
     { 
      throw new OverflowException(Environment.GetResourceString("Overflow_Int32")); 
     } 
    } 
    else 
    { 
     if (!Number.NumberToInt32(ref numberBuffer, ref result)) 
     { 
      throw new OverflowException(Environment.GetResourceString("Overflow_Int32")); 
     } 
    } 
    return result; 
} 

private unsafe static void StringToNumber(string str, NumberStyles options, ref Number.NumberBuffer number, NumberFormatInfo info, bool parseDecimal) 
{ 
    if (str == null) 
    { 
     throw new ArgumentNullException("String"); 
    } 
    fixed (char* ptr = str) 
    { 
     char* ptr2 = ptr; 
     if (!Number.ParseNumber(ref ptr2, options, ref number, info, parseDecimal) || ((ptr2 - ptr/2)/2 < str.Length && !Number.TrailingZeros(str, (ptr2 - ptr/2)/2))) 
     { 
      throw new FormatException(Environment.GetResourceString("Format_InvalidString")); 
     } 
    } 
} 
+0

Interesante. @JonHarrop debe medir el rendimiento de usar su intérprete int más simple en una versión de subcadena, solo para asegurarse de que la causa raíz del 5x se comprenda completamente. – fmr

+0

Probablemente sea un poco de ambos. En mi situación similar, obtuve mejoras de rendimiento a partir de un análisis de enteros personalizado (porque podría asumir un solo formato) y de no cortar mi cadena de entrada. – MrKWatkins

+0

@fmr "@JonHarrop debe medir el rendimiento de usar su analizador int más simple en una versión de subcadena, solo para asegurarse de que la causa raíz del 5x se comprenda completamente". Dividir en subcadenas toma 0.7s. Dividir en subcadenas y asignar mi analizador int rápido sobre ellos toma 0.99s. Sin embargo, hay otras fuerzas trabajando aquí. –

4

Pegar todo este código en C# y llaman Test(). Esto es lo más cerca que puede llegar a operar directamente en la matriz de cadenas para analizar números usando C#. Está construido para la velocidad, no elegancia.Se crearon las funciones ParseInt y ParseFloat para que un motor de gráficos OpenGL pueda importar vectores de modelos 3D basados ​​en texto. El análisis de flotadores es un importante cuello de botella en ese proceso. Esto fue tan rápido como pude hacerlo.

using System.Diagnostics; 

    private void Test() 
    { 
     Stopwatch sw = new Stopwatch(); 
     StringBuilder sb = new StringBuilder(); 
     int iterations = 1000; 

     // Build a string of 1000000 space separated numbers 
     for (var n = 0; n < iterations; n++) 
     { 
      if (n > 0) 
       sb.Append(' '); 
      sb.Append(n.ToString()); 
     } 

     string numberString = sb.ToString(); 

     // Time the process 
     sw.Start(); 
     StringToInts(numberString, iterations); 
     //StringToFloats(numberString, iterations); 
     sw.Stop(); 
     long proc1 = sw.ElapsedMilliseconds; 

     Console.WriteLine("iterations: {0} \t {1}ms", iterations, proc1); 
    } 

    private unsafe int[] StringToInts(string s, int length) 
    { 
     int[] ints = new int[length]; 
     int index = 0; 
     int startpos = 0; 

     fixed (char* pStringBuffer = s) 
     { 
      fixed (int* pIntBuffer = ints) 
      { 
       for (int n = 0; n < s.Length; n++) 
       { 
        if (s[n] == ' ' || n == s.Length - 1) 
        { 
         if (n == s.Length - 1) 
          n++; 
         // pIntBuffer[index++] = int.Parse(new string(pStringBuffer, startpos, n - startpos)); 
         pIntBuffer[index++] = ParseInt((pStringBuffer + startpos), n - startpos); 
         startpos = n + 1; 
        } 
       } 
      } 
     } 

     return ints; 
    } 

    private unsafe float[] StringToFloats(string s, int length) 
    { 
     float[] floats = new float[length]; 
     int index = 0; 
     int startpos = 0; 

     fixed (char* pStringBuffer = s) 
     { 
      fixed (float* pFloatBuffer = floats) 
      { 
       for (int n = 0; n < s.Length; n++) 
       { 
        if (s[n] == ' ' || n == s.Length - 1) 
        { 
         if (n == s.Length - 1) 
          n++; 

         pFloatBuffer[index++] = ParseFloat((pStringBuffer + startpos), n - startpos); // int.Parse(new string(pStringBuffer, startpos, n - startpos)); 
         startpos = n + 1; 
        } 
       } 
      } 
     } 

     return floats; 
    } 

    public static unsafe int ParseInt(char* input, int len) 
    { 
     int pos = 0;   // read pointer position 
     int part = 0;   // the current part (int, float and sci parts of the number) 
     bool neg = false;  // true if part is a negative number 

     int* ret = stackalloc int[1]; 

     while (pos < len && (*(input + pos) > '9' || *(input + pos) < '0') && *(input + pos) != '-') 
      pos++; 

     // sign 
     if (*(input + pos) == '-') 
     { 
      neg = true; 
      pos++; 
     } 

     // integer part 
     while (pos < len && !(input[pos] > '9' || input[pos] < '0')) 
      part = part * 10 + (input[pos++] - '0'); 

     *ret = neg ? (part * -1) : part; 
     return *ret; 
    } 

    public static unsafe float ParseFloat(char* input, int len) 
    { 
     //float ret = 0f;  // return value 
     int pos = 0;   // read pointer position 
     int part = 0;   // the current part (int, float and sci parts of the number) 
     bool neg = false;  // true if part is a negative number 

     float* ret = stackalloc float[1]; 

     // find start 
     while (pos < len && (input[pos] < '0' || input[pos] > '9') && input[pos] != '-' && input[pos] != '.') 
      pos++; 

     // sign 
     if (input[pos] == '-') 
     { 
      neg = true; 
      pos++; 
     } 

     // integer part 
     while (pos < len && !(input[pos] > '9' || input[pos] < '0')) 
      part = part * 10 + (input[pos++] - '0'); 

     *ret = neg ? (float)(part * -1) : (float)part; 

     // float part 
     if (pos < len && input[pos] == '.') 
     { 
      pos++; 
      double mul = 1; 
      part = 0; 

      while (pos < len && !(input[pos] > '9' || input[pos] < '0')) 
      { 
       part = part * 10 + (input[pos] - '0'); 
       mul *= 10; 
       pos++; 
      } 

      if (neg) 
       *ret -= (float)part/(float)mul; 
      else 
       *ret += (float)part/(float)mul; 

     } 

     // scientific part 
     if (pos < len && (input[pos] == 'e' || input[pos] == 'E')) 
     { 
      pos++; 
      neg = (input[pos] == '-'); pos++; 
      part = 0; 
      while (pos < len && !(input[pos] > '9' || input[pos] < '0')) 
      { 
       part = part * 10 + (input[pos++] - '0'); 
      } 

      if (neg) 
       *ret /= (float)Math.Pow(10d, (double)part); 
      else 
       *ret *= (float)Math.Pow(10d, (double)part); 
     } 

     return (float)*ret; 
    } 
+3

Para aquellos que han encontrado esto en google, aunque esta es la solución elegida, las pruebas destacan que este varios problemas: StringToFloats llama a ParseInt, no a ParseFloat, siempre pierde el último número de la cadena y se bloquea si especifica menos números que están en la cuerda. ParseFloat a menudo produce números muy incorrectos, porque mul se desborda y no tiene sentido. p.ej. "0.000167039223" se analiza en 0.0011846202. ParseFloat pierde precisión con varios otros números porque los intermedios se guardan como flotantes que no pueden contenerlos exactamente. – user495625

+1

@ user495625 Seguí adelante y sin fecha la función ParseFloat porque estaba produciendo resultados erráticos con números de mayor precisión. La respuesta fue cambiar la variable 'mul' de un int a un doble. La precisión de la función es 10 decimales ahora. No voy a hacer que el resto sea a prueba de balas porque perdería su utilidad ilustrativa. Espero que financien esta respuesta útil sin embargo. Cuídate. – drankin2112

+0

@ drankin2112 Noté que esto aún recorta el último dígito de la cadena de entrada, ¿hay alguna solución para esto? También estoy intentando usar este fragmento para cargar modelos. – Krythic

1

Por lo tanto, no son funciones integradas para analizar directamente de una subcadena dentro de una cadena (es decir usando el inicio dado y longitud de una cadena) con el fin de evitar que se genere una cadena temporal ? Si no, ¿hay alguna biblioteca que brinde funciones eficientes para hacer esto?

Parece que desea usar un tampón léxico y un analizador léxico, similar a lo que OCaml puede proporcionarle ocamllex y el tampón Lexbuf. (No puedo proporcionar referencias para F #.)

Si su punto de referencia involucra una gran cantidad de enteros separados por otros tokens es su caso típico, funcionará bien. Pero en otras situaciones, podría ser poco práctico.

+0

@ user2314737 ¿Qué quieres decir? ¡No escribí una pregunta! – user40989

+0

No realmente. Lexing solo copiará cada subcadena que contenga un int en su propia cadena. Todavía necesitaría analizar esas cadenas en números reales ... –

+0

@JonHarrop Tienes razón, por supuesto. No tenía mi té en ese momento. – user40989

Cuestiones relacionadas