Rendimiento Introducción a la Computación Clase 18 Patricia Borensztejn Un programa ejemplo from random import randrange def rellena (talla, rango): valores = [0] * talla for i in range(talla): valores[i] = randrange(0, rango) return valores def burbuja (valores): nuevo = valores[:] for i in range(len(nuevo)): for j in range(len(nuevo)-1-i): if nuevo[j] > nuevo[j+1]: nuevo[j], nuevo[j+1] = nuevo[j+1], nuevo[j] print "Pasada %d " % i print nuevo return nuevo Algoritmo de la Burbuja # Programa principal talla = 1000 rango = 100000 vector = rellena(talla, rango) print "Vector desordenado: ", vector ordenado = burbuja(vector) print "Vector ordenado: ", ordenado Vector desordenado: [1, 28, 54, 84, 89, 59, 11, 9, 50, 53] Pasada 0 [1, 28, 54, 84, 59, 11, 9, 50, 53, 89] Pasada 1 [1, 28, 54, 59, 11, 9, 50, 53, 84, 89] Pasada 2 [1, 28, 54, 11, 9, 50, 53, 59, 84, 89] Pasada 3 [1, 28, 11, 9, 50, 53, 54, 59, 84, 89] Pasada 4 [1, 11, 9, 28, 50, 53, 54, 59, 84, 89] Pasada 5 [1, 9, 11, 28, 50, 53, 54, 59, 84, 89] Pasada 6 [1, 9, 11, 28, 50, 53, 54, 59, 84, 89] Pasada 7 [1, 9, 11, 28, 50, 53, 54, 59, 84, 89] Pasada 8 [1, 9, 11, 28, 50, 53, 54, 59, 84, 89] Pasada 9 [1, 9, 11, 28, 50, 53, 54, 59, 84, 89] Vector ordenado: [1, 9, 11, 28, 50, 53, 54, 59, 84, 89] Medición de tiempos • Utilizamos la función clock() que está en el módulo time. • La función clock() (en Windows y llamada por python) devuelve el número de segundos transcurridos desde que se inicia el proceso hasta la invocación de ella misma. • Probemos: >>> from time import clock >>> print clock() 1.62086901271e-06 >>> print clock() 5.41601507732 >>> print clock() 7.46666723397 >>> print clock() 10.0847228233 >>> print clock() 11.1570711221 >>> print clock() 12.2909208643 >>> print clock() 14.0760029532 >>> print clock() 15.2212307907 >>> print clock() 16.2983776722 Medidas de Tiempo • • • • • Segundo 1 Milisegundo= 1ms=10-3 segundos=0,001 seg 1 Microsegundo= 1us=10-6 segundos=0,000 001 seg 1 Nanosegundo= 1ns=10-9 segundos=0,000 000 001 seg 1 Picosegundo= 1ps=10-12 segundos=0,000 000 000 001 seg Midiendo con clock() from random import randrange from time import clock …… # Programa principal talla = 10 rango = 100 vector = rellena(talla, rango) print "Vector desordenado: ", vector tiempo_inicial = clock() ordenado = burbuja(vector) tiempo_final = clock() print "Vector ordenado: ", ordenado print "Tiempo de ejecucion de burbuja: %f " % (tiempo_final - tiempo_inicial) • ¿Que observamos? Midiendo con clock() C:\Users\Patricia\programas_python>python burbuja.py Vector desordenado: [50, 82, 54, 14, 41, 23, 94, 1, 45, 79] Vector ordenado: [1, 14, 23, 41, 45, 50, 54, 79, 82, 94] Tiempo de ejecucion de burbuja: 0.000027 C:\Users\Patricia\programas_python>python burbuja.py Vector desordenado: [23, 99, 72, 20, 68, 50, 95, 55, 75, 10] Vector ordenado: [10, 20, 23, 50, 55, 68, 72, 75, 95, 99] Tiempo de ejecucion de burbuja: 0.000041 C:\Users\Patricia\programas_python>python burbuja.py Vector desordenado: [39, 73, 55, 33, 21, 91, 8, 57, 85, 99] Vector ordenado: [8, 21, 33, 39, 55, 57, 73, 85, 91, 99] Tiempo de ejecucion de burbuja: 0.000025 C:\Users\Patricia\programas_python>python burbuja.py Vector desordenado: [29, 30, 89, 17, 8, 15, 68, 9, 15, 8] Vector ordenado: [8, 8, 9, 15, 15, 17, 29, 30, 68, 89] Tiempo de ejecucion de burbuja: 0.000032 ¿Qué observamos? • Que el tiempo de ordenación es variable, ¿tendrá que ver con los datos? • Probemos: repetimos 10 veces con los mismos datos (También cambié el tamaño del vector) # Programa principal talla = 1000 rango = 100000 vector = rellena(talla, rango) for i in range(1,10): tiempo_inicial = clock() ordenado = burbuja(vector) tiempo_final = clock() print "Tiempo de ejecucion de burbuja: %f " % (tiempo_final - tiempo_inicial) ¿Qué observamos? • Medimos: Tiempo de ejecucion de burbuja: 0.155590 Tiempo de ejecucion de burbuja: 0.160534 Tiempo de ejecucion de burbuja: 0.173138 Tiempo de ejecucion de burbuja: 0.105864 Tiempo de ejecucion de burbuja: 0.102857 Tiempo de ejecucion de burbuja: 0.101671 Tiempo de ejecucion de burbuja: 0.102848 Tiempo de ejecucion de burbuja: 0.102689 Tiempo de ejecucion de burbuja: 0.102279 • Observamos que hay variación y que no depende de los datos. Esto nos indica que para obtener un valor del tiempo de ejecución debemos ejecutar varias veces el programa y luego tomar la media. ¿Qué mas observamos? • Medimos: Tiempo de ejecucion de burbuja: 0.155590 Tiempo de ejecucion de burbuja: 0.160534 Tiempo de ejecucion de burbuja: 0.173138 Tiempo de ejecucion de burbuja: 0.105864 Tiempo de ejecucion de burbuja: 0.102857 Tiempo de ejecucion de burbuja: 0.101671 Tiempo de ejecucion de burbuja: 0.102848 Tiempo de ejecucion de burbuja: 0.102689 Tiempo de ejecucion de burbuja: 0.102279 • Observamos que la salida no tiene mas que seis decimales: esa es la resolución que podemos obtener en python. Entonces, ¿cuantas veces repetimos la medición? • Podríamos repetir las mediciones durante un tiempo mínimo, y luego hacer la media con los valores obtenidos. El tiempo mínimo dependería de nuestro programa Media del Tiempo de ejecución # Programa principal talla = 10 rango = 100000 tiempo_minimo = 10 repeticiones= 0 vector = rellena(talla, rango) tiempo_inicial = tiempo_final =clock() while tiempo_final - tiempo_inicial < tiempo_minimo: ordenado = burbuja(vector) repeticiones+=1 tiempo_final = clock() print "Tiempo de ejecucion de burbuja: %f " %\ ((tiempo_final - tiempo_inicial)/repeticiones) ¿De qué depende? • Del Procesador y del Sistema Operativo, es decir, del soporte hardware que ofrece el procesador y de cómo el SO usa esta información. • También de las librerías usadas por los lenguajes de programación. • Pero además, el tiempo de ejecución de un programa en un procesador de propósito general NO es constante. Tiempo de ejecución variable • El tiempo de ejecución de un conjunto de instrucciones es variable en un procesador típico debido a que: – Las instrucciones de nuestro programa NO se ejecutan en secuencia, sino desordenamente. Esto es así porque los modernos procesadores actuales siguen el modelo OOO (Out Of Order). Su tiempo de ejecución, para cualquier programa, NO es constante. (Esta es una carácterística muy indeseable para algunos sistemas de tiempo real, donde nos gustaría que el tiempo de procesamiento sea constante) – Los datos y las instrucciones residen en memoria principal, pero cuando se ejecutan por primera vez son traídas cerca del procesador, a la memoria caché. En un procesador moderno hay varias jerarquías de memoria caché, y el llenado y vaciado de estas estructuras hacen variable el tiempo de ejecución. – Los programas cuando se ejecutan NO residen completamente en Memoria Principal. Cuando hay que acceder al disco a buscar el resto del programa, el SO detiene la ejecución del programa. Cuando medimos con clock(), la rutina no devuelve el tiempo transcurrido real, sino el tiempo que la CPU dedicó a ese proceso. Midiendo en C • También tenemos la función clock(), pero ésta devuelve el tiempo medido en ciclos de procesador. Para obtener el tiempo de ejecución en segundos debemos dividir por una constante llamada CLOCKS_PER_SEC. • Para averiguar el tiempo en unidades de tiempo (segundos, por ejemplo) hay que dividir el número de ciclos devueltos por clock() (que es un entero long) por el número de ciclos por segundo de la máquina en la que ejecutamos. • En realidad, la librería time que contiene a la función clock() tiene una constante CLOCKS_PER_SEC. Podemos averiguar cuanto vale en nuestro sistema. (Vale 1000) • Y otra cosa: clock() de Python usa clock() de la librería de C, solo que después hace una conversión a segundos, por lo tanto el resultado es un float. Tiempo de Ejecución • La duración del ciclo de reloj viene dada por la frecuencia del procesador ciclos de reloj T_ejec frecuencia del reloj • Frecuencia del reloj = ciclos por segundo (1 Hz. = 1 ciclo/sec). • Frecuencia es la inversa del período del reloj. • Un reloj de 200 Mhz, tiene una duración de ciclo de ..............? 1 200 10 6 10 9 5 nanoseconds Tiempo de Ejecución • Tiempo de Ejecución medido en términos de ciclos de reloj T_ejec ciclos de reloj * duración del ciclo de reloj segundos ciclos segundos programa programa ciclo • Duración del ciclo = tiempo entre ticks de reloj (segundos por ciclo) es el período del reloj time • Clock() cuenta Ciclos o Tics de reloj Burbuja en C Medimos en C Tiempo medio de ejecucion de burbuja: 0.004471 Process returned 48 (0x30) execution time : 10.031 s Press any key to continue. • Ordenamiento de un vector de 1000 enteros con valores entre 0 y 100 000. • Tiempo de repeticiones: 10 segundos Medimos en Python Tiempo de ejecucion de burbuja: 0.101502 >>> • Ordenamiento de un vector de 1000 enteros con valores entre 0 y 100 000. • Tiempo de repeticiones: 10 segundos C vs. Python • Ya lo sabíamos: un programa en un lenguaje interpretado es menos eficiente que el mismo programa en un lenguaje compilado. • Para la misma cantidad de datos a ordenar, C es 23 veces más rápido que Python! Tejec en Python = 0.101502 Tejec en C = 0.004471 22,7 Clock() • Es una función de la librería estándar de C, no dependiente del SO, por lo tanto se puede utilizar en cualquier SO. • Su precisión está en el orden de los milisegundos. Para saberlo, ejecutar este código: Precisión de clock() int main() { clock_t t1, t2; t1 = t2 = clock(); // loop until t2 gets a different value while(t1 == t2){ t2 = clock(); } // print resolution of clock() printf ("precision = %.8f ms\n", (double) (t2 - t1) / CLOCKS_PER_SEC * 1000); • En mi máquina, intel core i5 2,5Gigahertz, 4Giga memoria, windows 7: 1ms Como funciona Clock() • Hay un reloj del sistema que interrumpe al procesador cada x milisegundos, en cada interrupción incrementa un contador de tiempo. (en mi máquina, x=1mseg). Ese valor es el que consulta la función clock(). El valor está dado en ticks del reloj del sistema. • Pero también hay un reloj del hardware, que cuenta ciclos verdaderos, es decir, que tiene la precisión de la frecuencia del procesador, en el orden de los nanosegundos. ¿Como accedemos a esos contadores hardware? ….. En lenguaje ensamblador… Reloj del procesador • Todos los procesadores son sistemas síncronos, es decir, tienen un reloj que sincroniza las operaciones: – Fetch, decode, execute • El reloj de mi procesador tiene un Tciclo=1/frecuencia= 1/2.5 Gigahertz =1/2.5*109 ciclos por seg =(1*109 nanoseg por segundo )/2.5*109 ciclos por seg = 0.4 nanoseg. Performance Counters • Dependiendo de la implementación, los procesadores tienen un conjunto de registros que se utilizan para medir rendimiento a muy bajo nivel, es decir, a nivel del reloj del procesador. • Estos registros pueden ser programados (por el sistema, es decir, son registros privilegiados) para contar una variada cantidad de eventos del programa: – Número de ciclos de procesador: lleva la cuenta de los ciclos consumidos por la cpu desde su arranque – Número de accesos a memoria, número de hits o misses en la memoria caché – Tiempo de un acceso a memoria. – Y muchisimos mas eventos Performance Counters • Windows inicialmente tenía unas funciones que podían consultar el valor de estos registros: – QueryPerformanceCounters(): devuelve el número de ticks – QueryPerformanceFrequency(): devuelve la frecuencia en ciclos por segundo • Ambas funciones ahora no consultan esos registros, aunque quizás son un poco mas precisas que clock() • Si necesitamos mucha precisión en nuestras medidas, tenemos que usar otra herramienta: gprof. Gprof • Es un profiler. Es decir, una herramienta (un programa) que realiza un análisis dinámico de un programa. Se debe preparar el programa para su ejecución mediante un profiler: Compilar: cc -g -c myprog.c utils.c -pg Enlazar: cc -o myprog myprog.o utils.o -pg Ejecutar: myprog • La salida de myprog es un archivo llamado gmon.out • Luego ejecutamos el profiler – gprof myprog.exe >salida • En el archivo salida, tendremos los datos de la ejecución del programa. Gprof Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 33.34 0.02 0.02 7208 0.00 0.00 open 16.67 0.03 0.01 244 0.04 0.12 offtime 16.67 0.04 0.01 8 1.25 1.25 memccpy 16.67 0.05 0.01 7 1.43 1.43 write 16.67 0.06 0.01 mcount 0.00 0.06 0.00 236 0.00 0.00 tzset 0.00 0.06 0.00 192 0.00 0.00 tolower 0.00 0.06 0.00 47 0.00 0.00 strlen 0.00 0.06 0.00 45 0.00 0.00 strchr 0.00 0.06 0.00 1 0.00 50.00 main 0.00 0.06 0.00 1 0.00 0.00 memcpy 0.00 0.06 0.00 1 0.00 10.11 print 0.00 0.06 0.00 1 0.00 0.00 profil 0.00 0.06 0.00 1 0.00 50.00 report Ejercicio de rendimiento (1) • Realizar varias mediciones para distintos tamaños de vectores, distintos algoritmos, y distintos lenguajes (C y Python) • Medir con clock() • Graficar los resultados, algo así: Ejercicio de rendimiento (2) • Utilizar un programa que esté construído con unas cuantas funciones. • Ejecutarlo con el profiler gprof (hay que ver si esta instalado) • Estudiar los resultados de la salida • Manual del gnu: – http://www.cs.utah.edu/dept/old/texinfo/as/gpro f.html#SEC1