¿Cómo puedo mejorar / reemplazar sprintf, que he medido como un punto de acceso de rendimiento?

StackOverflow https://stackoverflow.com/questions/271971

Pregunta

Mediante la elaboración de perfiles descubrí que el sprintf aquí lleva mucho tiempo. ¿Existe una alternativa de mejor rendimiento que aún maneje los ceros iniciales en los campos y / m / d h / m / s?

SYSTEMTIME sysTime;
GetLocalTime( &sysTime );
char buf[80];
for (int i = 0; i < 100000; i++)
{

    sprintf(buf, "%4d-%02d-%02d %02d:%02d:%02d",
        sysTime.wYear, sysTime.wMonth, sysTime.wDay, 
        sysTime.wHour, sysTime.wMinute, sysTime.wSecond);

}

Nota: El OP explica en los comentarios que este es un ejemplo simplificado. El " real " El bucle contiene código adicional que utiliza valores de tiempo variables de una base de datos. La creación de perfiles ha identificado a sprintf () como el infractor.

¿Fue útil?

Solución

Si estuviera escribiendo su propia función para hacer el trabajo, una tabla de búsqueda de los valores de cadena de 0 .. 61 evitaría tener que hacer ninguna aritmética para todo excepto el año.

editar: tenga en cuenta que para hacer frente a los segundos bisiestos (y para que coincida con strftime () ) debería poder imprimir segundos valores de 60 y 61.

char LeadingZeroIntegerValues[62][] = { "00", "01", "02", ... "59", "60", "61" };

Alternativamente, ¿qué tal strftime () ? No tengo idea de cómo se compara el rendimiento (bien podría estar llamando a sprintf ()), pero vale la pena mirarlo (y podría estar haciendo la búsqueda anterior en sí).

Otros consejos

Puede intentar llenar cada carácter en la salida a su vez.

buf[0] = (sysTime.wYear / 1000) % 10 + '0' ;
buf[1] = (sysTime.wYear / 100) % 10 + '0';
buf[2] = (sysTime.wYear / 10) % 10 + '0';
buf[3] = sysTime.wYear % 10 + '0';
buf[4] = '-';

... etc ...

No es bonito, pero te haces una idea. Por lo menos, puede ayudar a explicar por qué sprintf no va a ser tan rápido.

OTOH, tal vez podrías almacenar en caché el último resultado. De esa manera solo necesitaría generar uno por segundo.

Printf necesita lidiar con muchos formatos diferentes. Ciertamente, podría tomar la fuente de printf y usarla como base para rodar su propia versión que se ocupa específicamente de la estructura sysTime . De esa manera, pasa un argumento y hace exactamente el trabajo que debe hacerse y nada más.

¿Qué quieres decir con "largo"? tiempo, ya que sprintf () es la única instrucción en su bucle y la "plomería" del bucle (incremento, comparación) es insignificante, el sprintf () tiene para consumir la mayor parte del tiempo.

¿Recuerdas el viejo chiste sobre el hombre que perdió su anillo de bodas en 3rd Street una noche, pero lo buscó en 5th porque la luz era más brillante allí? Has creado un ejemplo que está diseñado para " probar " su suposición de que sprintf () no es eficiente.

Sus resultados serán más precisos si su perfil "real" código que contiene sprintf () además de todas las demás funciones y algoritmos que usa. Alternativamente, intente escribir su propia versión que aborde la conversión numérica específica con relleno de cero que necesita.

Te sorprenderán los resultados.

Parece que Jaywalker sugiere un método muy similar (ganarme en menos de una hora).

Además del método de tabla de búsqueda ya sugerido (matriz n2s [] a continuación), ¿qué tal generar el búfer de formato para que el sprintf habitual sea menos intenso? El siguiente código solo tendrá que completar el minuto y segundo cada vez a través del ciclo a menos que el año / mes / día / hora haya cambiado. Obviamente, si alguno de esos ha cambiado, usted recibe otro golpe de sprintf, pero en general puede no ser más de lo que está presenciando actualmente (cuando se combina con la búsqueda de matriz).


static char fbuf[80];
static SYSTEMTIME lastSysTime = {0, ..., 0};  // initialize to all zeros.

for (int i = 0; i < 100000; i++)
{
    if ((lastSysTime.wHour != sysTime.wHour)
    ||  (lastSysTime.wDay != sysTime.wDay)
    ||  (lastSysTime.wMonth != sysTime.wMonth)
    ||  (lastSysTime.wYear != sysTime.wYear))
    {
        sprintf(fbuf, "%4d-%02s-%02s %02s:%%02s:%%02s",
                sysTime.wYear, n2s[sysTime.wMonth],
                n2s[sysTime.wDay], n2s[sysTime.wHour]);

        lastSysTime.wHour = sysTime.wHour;
        lastSysTime.wDay = sysTime.wDay;
        lastSysTime.wMonth = sysTime.wMonth;
        lastSysTime.wYear = sysTime.wYear;
    }

    sprintf(buf, fbuf, n2s[sysTime.wMinute], n2s[sysTime.wSecond]);

}

¿Qué tal el almacenamiento en caché de los resultados? ¿No es esa una posibilidad? Teniendo en cuenta que esta llamada sprintf () en particular se realiza con demasiada frecuencia en su código, supongo que entre la mayoría de estas llamadas consecutivas, el año, el mes y el día no cambian.

Por lo tanto, podemos implementar algo como lo siguiente. Declare una estructura SYSTEMTIME antigua y actual:

SYSTEMTIME sysTime, oldSysTime;

Además, declare partes separadas para mantener la fecha y la hora:

char datePart[80];
char timePart[80];

Por primera vez, deberá completar tanto sysTime, oldSysTime como datePart y timePart. Pero los siguientes sprintf () se pueden hacer bastante más rápido como se indica a continuación:

sprintf (timePart, "%02d:%02d:%02d", sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
if (oldSysTime.wYear == sysTime.wYear && 
  oldSysTime.wMonth == sysTime.wMonth &&
  oldSysTime.wDay == sysTime.wDay) 
  {
     // we can reuse the date part
     strcpy (buff, datePart);
     strcat (buff, timePart);
  }
else {
     // we need to regenerate the date part as well
     sprintf (datePart, "%4d-%02d-%02d", sysTime.wYear, sysTime.wMonth, sysTime.wDay);
     strcpy (buff, datePart);
     strcat (buff, timePart);
}

memcpy (&oldSysTime, &sysTime, sizeof (SYSTEMTIME));

El código anterior tiene cierta redundancia para que el código sea más fácil de entender. Puedes factorizar fácilmente. Puede acelerar aún más si sabe que incluso la hora y los minutos no cambiarán más rápido que su llamada a la rutina.

Haría algunas cosas ...

  • guarde en caché la hora actual para que no tenga que volver a generar la marca de tiempo cada vez
  • hacer la conversión de tiempo manualmente. La parte más lenta de las funciones de la familia printf es el análisis de cadenas de formato, y es una tontería dedicar ciclos a ese análisis en cada ejecución de bucle.
  • intente usar tablas de búsqueda de 2 bytes para todas las conversiones ( {" 00 " ;, " 01 " ;, " 02 " ;, ..., " 99 "} ). Esto se debe a que desea evitar la aritmética modular, y una tabla de 2 bytes significa que solo tiene que usar un módulo por año.

Probablemente obtendrías un aumento de rendimiento al realizar manualmente una rutina que muestra los dígitos en el buf de retorno, ya que podrías evitar analizar repetidamente una cadena de formato y no tendrías que lidiar con muchos de los casos más complejos de los controladores de sprintf . Sin embargo, detesto recomendarlo.

Recomendaría intentar averiguar si de alguna manera puede reducir la cantidad que necesita para generar estas cadenas, si algunas veces son opcionales, se pueden almacenar en caché, etc.

Estoy trabajando en un problema similar en este momento.

Necesito registrar las declaraciones de depuración con marca de tiempo, nombre de archivo, número de línea, etc. en un sistema integrado. Ya tenemos un registrador en su lugar, pero cuando giro la perilla a 'registro completo', se come todos nuestros ciclos de proceso y pone nuestro sistema en estados extremos, lo que indica que ningún dispositivo informático debería tener que experimentar.

Alguien dijo "No se puede medir / observar algo sin cambiar lo que se está midiendo / observando".

Así que estoy cambiando las cosas para mejorar el rendimiento. El estado actual de las cosas es que soy 2 veces más rápido que la llamada de función original (el cuello de botella en ese sistema de registro no está en la llamada de función sino en el lector de registro que es un ejecutable separado, que puedo descartar) si escribo mi propia pila de registro).

La interfaz que necesito proporcionar es algo como: void log (int channel, char * filename, int lineno, format, ...) . Necesito agregar el nombre del canal (que actualmente realiza una búsqueda lineal dentro de una lista. ¡Para cada declaración de depuración!) Y la marca de tiempo, incluido el contador de milisegundos. Estas son algunas de las cosas que estoy haciendo para hacer esto más rápido:

  • Stringify nombre del canal para que pueda strcpy en lugar de buscar en la lista. defina la macro LOG (canal, ... etc.) como log (#channel, ... etc) . Puede usar memcpy si fija la longitud de la cadena definiendo LOG (canal, ...) log (" .... " # channel - sizeof (" .... " #channel) + * 11 *) para obtener 10 longitudes de canal fijas de bytes
  • Generar cadena de marca de tiempo un par de veces por segundo. Puedes usar asctime o algo así. Luego, memorice la cadena de longitud fija en cada instrucción de depuración.
  • Si desea generar la cadena de marca de tiempo en tiempo real, entonces una tabla de búsqueda con asignación (¡no memcpy!) es perfecta. Pero eso solo funciona para números de 2 dígitos y tal vez para el año.
  • ¿Qué pasa con tres dígitos (milisegundos) y cinco dígitos (lineno)? No me gusta itoa y no me gusta el itoa personalizado ( digit = ((value / = value)% 10) ) ya sea porque los divs y los mods son lentos . Escribí las funciones a continuación y luego descubrí que hay algo similar en el manual de optimización de AMD (en ensamblaje) que me da la confianza de que se trata de las implementaciones de C más rápidas.

    void itoa03(char *string, unsigned int value)
    {
       *string++ = '0' + ((value = value * 2684355) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = ' ';/* null terminate here if thats what you need */
    }
    

    Del mismo modo, para los números de línea,

    void itoa05(char *string, unsigned int value)
    {
       *string++ = ' ';
       *string++ = '0' + ((value = value * 26844 + 12) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = ' ';/* null terminate here if thats what you need */
    }
    

En general, mi código es bastante rápido ahora. El vsnprintf () que necesito usar toma aproximadamente el 91% del tiempo y el resto de mi código toma solo el 9% (mientras que el resto del código, es decir, excepto vsprintf () solía tomar 54% antes)

Los dos formateadores rápidos que he probado son FastFormat y Karma :: generate (parte de Boost Spirit ).

También puede resultarle útil compararlo o, al menos, buscar puntos de referencia existentes.

Por ejemplo este (aunque le falta FastFormat):

 Conversión de entero rápido a cadena en C ++

StringStream es la sugerencia que recibí de Google.

http://bytes.com/forum/thread132583.html

Es difícil imaginar que vas a vencer a sprintf al formatear enteros. ¿Estás seguro de que Sprintf es tu problema?

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top