Entendemos por benchmarking como el proceso por el cual se obtiene información útil que ayuda a una organización a mejorar sus procesos, con el objetivo de conseguir la máxima eficacia, ayudando a la empresa a moverse desde donde está hacia dónde quiere llegar.
Hoy vamos a suponer que, cada uno de vosotros, ha dejado su actual empleo, y se ha unido a un nuevo proyecto: un integrador de servicios de intercambio de criptomonedas.
El objetivo está muy claro: recoger periódicamente la información (criptomonedas disponibles, ratios de cambio, evoluciones, etc) de todos los servicios de intercambio de criptomonedas, para proporcionar, a nuestros usuarios, una visión global del mercado, y así puedan escoger dónde realizar sus intercambios.
Después de meses de duro trabajo, con los deadlines para anteayer, tenemos un sistema que funciona a la perfección, el código es bastante espagueti pero está bien cubierto con tests automatizados.
A pesar de su perfecto funcionamiento, hoy ha llegado el jefe del proyecto a la oficina con una mala noticia:
El proceso de sincronización de los datos extraídos de los servicios de intercambio es demasiado lento, nuestra competencia está siendo capaz de proporcionar información con una mayor frecuencia de actualización.
Benchmarking
Como vimos anteriormente (en la definición del término), el benchmarking nos permitirá analizar nuestro proceso de sincronización, cuantificar la distancia en términos de eficiencia temporal entre nuestro proceso y el de la competencia, y ver cuál es el resultado obtenido en cada iteración del proceso de mejora.
Por el momento vamos a suponer que el método a analizar es el siguiente:
func (i Integrator) Synchronize() []Exchange {
...
}
Bien, pues para escribir nuestro primer benchmark, vamos a hacer uso del paquete testing que ya vimos en el artículo de introducción a los tests automatizados. Éste paquete, marcado por la filosofia de Go de proveernos con todas las herramientas de tooling necesarias en el core del propio lenguaje, no solo nos proporciona las herramientas necesarias para los tests automatizados sino que también nos proporciona las herramientas necesarias para realizar nuestros procesos de benchmarking.
Veamos un ejemplo:
func BenchmarkSynchronize(b *testing.B) {
for n := 0; n < b.N; n++ {
i := NewIntegrator()
i.Synchronize()
}
}
En esta ocasión, vamos a hacer uso de un puntero de testing.B, que nos permitirá obtener información acerca de la ejecución de nuestros métodos y procesos. Si os fijáis, lo que hacemos en nuestro benchmark, no es simplemente ejecutar la función en cuestión, sino que estamos haciendo un bucle con N ejecuciones de la función con la ayuda de testing.B.
Esto puede parecer una tonteria, sin embargo, la importancia de éste elemento es vital, pues será él el que determinará el número suficiente de ejecuciones a realizar de nuestra función para obtener información de calidad. Además, de ésta forma tendrá la capacidad de detener el proceso en caso de que surja algún imprevisto.
Bien, ahora que nuestro benchmark ya podría ser lanzado, veamos cómo hacer un pequeño refactor para mejorarlo un poco:
func BenchmarkSynchronize(b *testing.B) {
i := NewIntegrator()
b.ResetTimer()
for n := 0; n < b.N; n++ {
i.Synchronize()
}
}
¡Ahora mejor! Podemos observar dos cambios:
- Por un lado, hemos sacado la inicialización del integrador fuera del bucle, pues, por ahora, esa parte no la queremos analizar: solo nos queremos centrar en el
ei.Synchronize()
. - Por otro lado, hemos usado la sentencia
b.ResetTimer()
para resetear el temporizador interno detesting.B
.
Éste último cambio solo será necesario cuándo el proceso de inicialización previo (en este caso la inicialización del integrador) tenga un coste temporal significativo, y, lo vamos a usar, con el objetivo de que ese tiempo no sea computado dentro de nuestro benchmark, con el fin de obtener una información de mayor calidad.
Lanzando nuestros benchmarks
Ahora que ya tenemos nuestro primer benchmark listo, vamos a lanzarlo para obtener algunos datos:
go test -bench=.
Si tuviéramos varios benchmarks, también podríamos hacer uso del argumento -run
como ya vimos con los tests automatizados:
go test -run=Synchronize -bench=.
La salida debería ser similar a:
goos: linux
goarch: amd64
pkg: github.com/friendsofgo/workspace/exchanges
BenchmarkSynchronize-4 2000000000 3500137043 ns/op
PASS
ok github.com/friendsofgo/workspace/exchanges 3.503s
Esta información será la que, posteriormente, podremos usar para ver el impacto de nuestros cambios.
Midiendo el impacto de nuestros cambios
Una vez ya sabemos cómo definir nuestros benchmarks y cómo obtener información de ellos, el siguiente paso es explicar dicha información. Para ello vamos a hacer uso de la herramienta benchcmp, que puede ser instalada mediante:
go get -u golang.org/x/tools/cmd/benchcmp
Ahora solo nos queda guardar los resultados de las ejecuciones de nuestros benchmarks con diferentes implementaciones de nuestro sistema:
# check out the older implementation
go test -run=Synchronize -bench=. > bench.old
# check out the newer implementation
go test -run=Synchronize -bench=. > bench.new
Y comparar:
benchcmp bench.old bench.new
Lo que nos dará una salida similar a:
benchmark old ns/op new ns/op delta
BenchmarkSynchronize-4 3500241284 2000232442 -42.85%
Cómo podemos observar, en esta ocasión, por ejemplo, nuestros cambios han tenido un impacto (positivo) que supone una reducción del tiempo de ejecución a un 42.85% menos que la versión original.
Next step: profiling
Ahora ya sabemos como comparar diferentes versiones de nuestro proceso de sincronización, por lo tanto, ya seremos capaces de empezar a realizar cambios en el mismo y determinar si éstos tienen un impacto positivo o negativo sobre el tiempo de ejecución del mismo.
Sin embargo, realizar cambios a ciegas, es un proceso bastante tedioso e ineficiente. Pues, aunque en algunas ocasiones seremos capaces de intuir que partes de nuestro código son las más lentas, en otras ocasiones será más complicado. Será en ese momento cuándo vamos a necesitar herramientas que sean capaces de guiarnos hacía qué partes de código son las primeras que deberíamos intentar optimizar.
¿Os imagináis poder obtener gráficas de éste estilo sobre vuestro código?
¡Pues eso será lo que veremos en el próximo capítulo de la serie “Analizando el rendimiento de tus aplicaciones Go”!