2010-06-27 9 views
8

En SO, hay bastantes preguntas sobre el perfil de rendimiento, pero no parece encontrar la imagen completa. Hay bastantes problemas involucrados y la mayoría de los Q & A ignoran todos menos unos pocos a la vez, o no justifican sus propuestas.¿La mejor manera de probar la velocidad del código en C++ sin profiler, o no tiene sentido intentarlo?

Lo que me estoy preguntando. Si tengo dos funciones que hacen lo mismo y tengo curiosidad acerca de la diferencia de velocidad, ¿tiene sentido probar esto sin herramientas externas, con temporizadores, o las pruebas compiladas afectarán demasiado los resultados?

Lo pregunto porque si es sensato, como programador de C++, quiero saber cómo se debe hacer mejor, ya que son mucho más simples que usar herramientas externas. Si tiene sentido, sigamos con todas las trampas posibles:

Considere este ejemplo. El siguiente código muestra 2 formas de hacer lo mismo:

#include <algorithm> 
#include <ctime> 
#include <iostream> 

typedef unsigned char byte; 

inline 
void 
swapBytes(void* in, size_t n) 
{ 
    for(size_t lo=0, hi=n-1; hi>lo; ++lo, --hi) 

     in[lo] ^= in[hi] 
    , in[hi] ^= in[lo] 
    , in[lo] ^= in[hi] ; 
} 

int 
main() 
{ 
     byte arr[9]  = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }; 
    const int  iterations = 100000000; 
     clock_t begin  = clock(); 

    for(int i=iterations; i!=0; --i) 

     swapBytes(arr, 8); 

    clock_t middle = clock(); 

    for(int i=iterations; i!=0; --i) 

     std::reverse(arr, arr+8); 

    clock_t end = clock(); 

    double secSwap = (double) (middle-begin)/CLOCKS_PER_SEC; 
    double secReve = (double) (end-middle )/CLOCKS_PER_SEC; 


    std::cout << "swapBytes, for: " << iterations << " times takes: " << middle-begin 
      << " clock ticks, which is: " << secSwap << "sec."   << std::endl; 

    std::cout << "std::reverse, for: " << iterations << " times takes: " << end-middle 
      << " clock ticks, which is: " << secReve << "sec."   << std::endl; 

    std::cin.get(); 
    return 0; 
} 

// Output: 

// Release: 
// swapBytes, for: 100000000 times takes: 3000 clock ticks, which is: 3sec. 
// std::reverse, for: 100000000 times takes: 1437 clock ticks, which is: 1.437sec. 

// Debug: 
// swapBytes, for: 10000000 times takes: 1781 clock ticks, which is: 1.781sec. 
// std::reverse, for: 10000000 times takes: 12781 clock ticks, which is: 12.781sec. 

Las cuestiones:

  1. de los temporizadores usar y cómo hacer el tiempo de CPU realmente consumida por el código en la pregunta?
  2. ¿Cuáles son los efectos de la optimización del compilador (dado que estas funciones simplemente intercambian bytes de ida y vuelta, lo más eficiente es, obviamente, no hacer nada en absoluto)?
  3. Considerando los resultados presentados aquí, ¿cree que son precisos (puedo asegurarle que múltiples ejecuciones dan resultados muy similares)? En caso afirmativo, puede explicar cómo std :: reverse llega a ser tan rápido, teniendo en cuenta la simplicidad de la función personalizada. No tengo el código fuente de la versión vC++ que utilicé para esta prueba, pero here is the implementation de GNU. Todo se reduce a la función iter_swap, que es completamente incomprensible para mí. También se espera que esto se ejecute dos veces más rápido que esa función personalizada, y si es así, ¿por qué?

contemplaciones:

  1. parece se proponen dos contadores de tiempo de alta precisión: clock() y QueryPerformanceCounter (en Windows). Obviamente, nos gustaría medir el tiempo de CPU de nuestro código y no el tiempo real, pero por lo que yo entiendo, estas funciones no dan esa funcionalidad, por lo que otros procesos en el sistema interferirían con las mediciones. This page en la biblioteca gnu c parece contradecir eso, pero cuando pongo un punto de interrupción en vC++, el proceso depurado obtiene una gran cantidad de marcas de reloj a pesar de que se suspendió (no he probado en gnu). ¿Me faltan contadores alternativos para esto o necesitamos al menos bibliotecas o clases especiales para esto? Si no es así, ¿el reloj es lo suficientemente bueno en este ejemplo o habría una razón para usar QueryPerformanceCounter?

  2. ¿Qué podemos saber con certeza sin herramientas de depuración, desensamblaje y creación de perfiles? ¿Está pasando algo realmente? ¿La llamada a la función está en línea o no? Al verificar el depurador, los bytes realmente se intercambian, pero prefiero saber por la teoría por qué, que a partir de las pruebas.

Gracias por cualquier instrucción.

actualización

Gracias a una hint de tojas la función swapBytes ahora corre más rápido que el std :: inversa. No me había dado cuenta de que la copia temporal en caso de un byte debe ser solo un registro, y por lo tanto es muy rápido. La elegancia puede cegarte.

inline 
void 
swapBytes(byte* in, size_t n) 
{ 
    byte t; 

    for(int i=0; i<7-i; ++i) 
    { 
     t  = in[i]; 
     in[i] = in[7-i]; 
     in[7-i] = t; 
    } 
} 

Gracias a una tip de ChrisW he encontrado que en las ventanas se puede obtener el tiempo real de la CPU consumidos por una (es decir: tu) proceso a través Windows Management Instrumentation. Esto definitivamente parece más interesante que el contador de alta precisión.

+0

¿Qué SO estás preguntando? Cuando escribí el código de tiempo, los diversos sistemas operativos tenían llamadas API diferentes para el reloj correcto. –

+0

Estoy probando en WindowsXP, pero sería igualmente interesante escuchar acerca de otros SO's – nus

+0

que vale la pena probar sin un generador de perfiles, después de probar con un generador de perfiles. –

Respuesta

4

Obviamente nos gustaría medir el tiempo de CPU de nuestro código y no en tiempo real, pero por lo que entiendo, estas funciones no le da esa funcionalidad, por lo que otros procesos en el sistema interferirían con las mediciones.

hago dos cosas, para asegurar que el tiempo del reloj de pared y el tiempo de CPU son aproximadamente lo mismo:

  • de prueba por un período significativo de tiempo, es decir, varios segundos (por ejemplo, mediante la prueba de un bucle de miles de iteraciones)

  • Pruebe cuando la máquina está más o menos relativamente inactiva excepto para lo que estoy probando.

Alternativamente, si se desea medir solamente/con más exactitud el tiempo de CPU por hilo, que está disponible como un contador de rendimiento (véase, por ejemplo perfmon.exe).

¿Qué podemos saber con certeza sin depurar, dissassembling y herramientas de perfilado?

Casi nada (excepto que las E/S tienden a ser relativamente lentas).

+0

perfmon, sí, gracias por recordarme. Sabía que existía, y es bastante conveniente, pero ¿sabes si hay llamadas al sistema que podemos utilizar para obtener esta información dentro de nuestro programa? – nus

+0

@ufotds - Cuando lo hice, hace mucho tiempo, usé llamadas peludas para leer la sección oculta de "rendimiento" del registro (las llamadas fueron fáciles, pero el análisis de los datos binarios que devolvieron no lo fue). Hoy en día, podría, no sé, ser abstraído por la API "WMI". – ChrisW

1

¿Hay algo que tenga en contra de los perfiladores? Ellos ayudan mucho Dado que estás en WinXP, realmente deberías intentar probar vtune. Pruebe una prueba de muestreo de gráfico de llamadas y observe el tiempo de autoaprendizaje y el tiempo total de las funciones que se están llamando. No hay mejor manera de sintonizar su programa para que sea lo más rápido posible sin ser un genio de montaje (y uno realmente excepcional).

Algunas personas parecen ser alérgicas a los perfiladores. Yo solía ser uno de esos y pensé que sabía más acerca de dónde estaban mis puntos de acceso. A menudo tenía razón acerca de ineficiencias algorítmicas obvias, pero prácticamente siempre era incorrecto sobre más casos de micro-optimización. Simplemente reescribir una función sin cambiar la lógica (por ejemplo, reordenar cosas, colocar un código de caso excepcional en una función separada, no en línea, etc.) puede hacer que las funciones sean una docena de veces más rápidas e incluso los mejores expertos en desensamblaje generalmente no pueden predecir sin el generador de perfiles.

En cuanto a confiar solo en pruebas de tiempo simplistas, son extremadamente problemáticas. Esa prueba actual no es tan mala, pero es un error muy común escribir pruebas de tiempo de forma tal que el optimizador optimice el código muerto y termine probando el tiempo que se necesita para hacer esencialmente un nop o incluso nada en absoluto. Debe tener algunos conocimientos para interpretar el desmontaje para asegurarse de que el compilador no está haciendo esto.

También las pruebas de temporización como esta tienden a sesgar los resultados significativamente ya que muchas solo implican ejecutar el código una y otra vez en el mismo ciclo, lo que simplemente prueba el efecto de su código cuando toda la memoria el caché con toda la predicción de bifurcación funcionando perfectamente para él. A menudo solo te muestra los mejores escenarios posibles sin mostrarte el caso promedio del mundo real.

Dependiendo de las pruebas de tiempo del mundo real es un poco mejor; algo más cercano a lo que su aplicación estará haciendo a un alto nivel. No le dará detalles sobre lo que está demorando cuánto tiempo, pero eso es precisamente lo que el perfilador debe hacer.

+0

Antes usaba perfiladores para optimizar el rendimiento de programas completos, pero dada la curiosidad sobre algunas funciones simples, llamar a algunos contadores de tiempo es definitivamente menos complicado que elegir, descargar, instalar, leer el manual y trabajar con un generador de perfiles. En general, existe una diferencia entre entender cosas subyacentes como esta y lograr que su software funcione a un rendimiento razonable. Para este último, felizmente usaría un generador de perfiles y la velocidad de std :: reverse probablemente no me preocupara en absoluto, a menos que estuviese invirtiendo gigabytes ... – nus

+0

Si solo busca un rendimiento aceptable y un rendimiento no excepcional, entonces una prueba de tiempo podría hacer. Sin embargo, es importante tener en cuenta que, si bien un generador de perfiles puede tomar un poco de tiempo para aprender, es algo que tienes que hacer una vez. En vtune, simplemente use el asistente de muestreo del gráfico de llamadas, seleccione su archivo exe y ejecútelo.La única parte difícil es que debe modificar la configuración de sus proyectos (http://software.intel.com/en-us/articles/performance-tools-for-software-developers-using-the-intel-compilers-with- vtune-analyzer-o-intel-thread-profiler /). Después de eso solo ejecuta y mira el gráfico. – stinky472

+0

... el tiempo propio le dirá cuánto tiempo gasta la CPU en un método de clase/función dado excluyendo llamadas a otras funciones/métodos, y el tiempo total le dará la cantidad total de tiempo invertido en una función/método, incluido el tiempo dedicado a llamar a otras funciones/métodos. Es como una prueba de tiempo, excepto que obtiene el tiempo dedicado a cada función llamada en su prueba, incluido el tiempo total que pasó en main. – stinky472

1

Supongo que cualquier persona lo suficientemente competente como para responder a todas sus preguntas está demasiado ocupado para responder a todas sus preguntas. En la práctica, probablemente sea más eficaz formular preguntas únicas y bien definidas. De esta manera, puede esperar obtener respuestas bien definidas que pueda recopilar y estar en camino hacia la sabiduría.

De todos modos, quizás pueda responder a su pregunta sobre qué reloj usar en Windows.

reloj() no se considera un reloj de alta precisión. Si observa el valor de CLOCKS_PER_SEC, verá que tiene una resolución de 1 milisegundo. Esto solo es adecuado si está cronometrando rutinas muy largas, o un ciclo con 10000 de iteraciones. Como usted señala, si intenta repetir un método simple 10000 veces para obtener un tiempo que pueda medirse con clock(), es probable que el compilador intervenga y optimice todo.

Así que, en realidad, el único reloj para utilizar es QueryPerformanceCounter()

2

Para responder a su pregunta principal, el algoritmo "inverso" solo intercambia elementos de la matriz y no opera en los elementos de la matriz.

2

¿Es seguro decir que hace dos preguntas?

  • ¿Cuál es más rápido y por cuánto?

  • ¿Y por qué es más rápido?

Para el primero, no es necesario temporizadores de alta precisión. Todo lo que necesita hacer es ejecutarlos "lo suficiente" y medir con temporizadores de baja precisión. (Estoy pasado de moda, mi reloj de pulsera tiene una función de cronómetro, y es totalmente suficiente.)

Por el segundo, seguramente puede ejecutar el código en un depurador y hacerlo en un solo paso en la instrucción nivel. Dado que las operaciones básicas son tan simples, podrá ver aproximadamente cuántas instrucciones se requieren para el ciclo básico.

Piensa simple. El rendimiento no es un tema difícil. Por lo general, las personas intentan encontrar problemas, para los cuales this is a simple approach.

+0

sí, más de 2 incluso ... pero por alguna razón el depurador visual no me dejaba entrar en std :: reverse, pero solo lo había intentado en modo de lanzamiento. Ahora en depuración funciona y realmente puedo ver que hace exactamente lo que escribí en la actualización de swapBytes, además de verificar poiners, etc ... – nus

2

Use QueryPerformanceCounter en Windows si necesita un tiempo de alta resolución. La precisión del contador depende de la CPU, pero puede aumentar hasta por pulso de reloj. Sin embargo, perfilar en operaciones del mundo real siempre es una mejor idea.

+0

También depende de cuándo se llama. Muchas CPU cambian la frecuencia del reloj de forma dinámica. –

-3

¿Qué? ¿Cómo medir la velocidad sin un generador de perfiles? El mismo acto de medir la velocidad es perfiles! La pregunta es: "¿cómo puedo escribir mi propio generador de perfiles?"Y la respuesta es claramente 'no'.

Además, usted debe utilizar std::swap, en primer lugar, lo que invalida toda esta persecución sin sentido completo.

-1 por falta de sentido.

+0

std :: reverse es un wrapper alrededor de std :: swap ... – nus

+2

No he bajado de categoría, pero una cosa que he aprendido en SO es facilitar las cosas a las personas. Todos entramos con diferentes niveles de antecedentes, y podemos compartir la sabiduría de los demás. Claramente tienes sabiduría para compartir. Eso es algo bueno de SO. –

+0

Mike: punto tomado. Eres más paciente que yo. Aparte de eso, ¿TÚ piensas que esta pregunta es válida? Estoy aprendiendo rápidamente que las preguntas sensatas son raras aquí. Las preguntas de optimización solo preocupan acerca de qué aplicaciones están programando estas personas. Espero que mi banco no esté empleando programadores para preguntarse si deberían lanzar su propio std :: swap. :) – John

2

(Esta respuesta es específica de Windows XP y el compilador de VC++ de 32 bits).

Lo más fácil para sincronizar pequeños bits de código es el contador de marca de tiempo de la CPU. Este es un valor de 64 bits, un recuento de la cantidad de ciclos de CPU que se ejecutan hasta ahora, que es una resolución tan buena como la que obtendrás. Los números reales obtienes un son especialmente útiles tal como están, pero si promedias varias carreras de varios enfoques competitivos, entonces puedes compararlos de esa manera. Los resultados son un poco ruidosos, pero aún válidos para propósitos de comparación.

Para leer el contador de marca de tiempo, utilice un código como el siguiente:

LARGE_INTEGER tsc; 
__asm { 
    cpuid 
    rdtsc 
    mov tsc.LowPart,eax 
    mov tsc.HighPart,edx 
} 

(La instrucción cpuid está ahí para asegurarse de que no hay instrucciones incompletas a la espera de completar.)

Hay cuatro cosas que vale la pena destacar sobre este enfoque.

En primer lugar, debido al lenguaje ensamblador en línea, no funcionará como está en el compilador x64 de MS. (Tendrá que crear un archivo .ASM con una función en él. Un ejercicio para el lector; no conozco los detalles)

En segundo lugar, para evitar problemas con los contadores de ciclos que no están sincronizados en diferentes núcleos/hilos/lo que usted tiene, puede que sea necesario establecer la afinidad de su proceso para que solo se ejecute en una unidad de ejecución específica. (De nuevo ... puede que no)

En tercer lugar, definitivamente querrá comprobar el lenguaje ensamblador generado para asegurarse de que el compilador genera aproximadamente el código que espera. Tenga cuidado con las partes de código que se eliminan, las funciones que se incluyen, ese tipo de cosas.

Finalmente, los resultados son bastante ruidosos. Los contadores de ciclos cuentan los ciclos en todo, incluso la espera de cachés, el tiempo dedicado a ejecutar otros procesos, el tiempo pasado en el propio sistema operativo, etc. Desafortunadamente, no es posible (solo en Windows) medir solo el tiempo de su proceso. Entonces, sugiero ejecutar el código bajo prueba muchas veces (varias decenas de miles) y calcular el promedio. Esto no es muy astuto, pero parece haber producido resultados útiles para mí en cualquier caso.

+0

hola, gracias por este fragmento. Dudo que sea un valor práctico para este propósito, ya que aparentemente con WMI * es * posible medir solo su proceso, pero lo pegué en un simple programa de C++ y funciona tal como está. Además de eso, es la primera vez que uso el ensamblador en línea, porque mi conocimiento como ensamblador es bastante sombrío ... – nus

Cuestiones relacionadas