Haz que tus programas Go sean rápidos con los perfiles

Go se usa a menudo para escribir sistemas distribuidos, almacenes de datos avanzados y microservicios. El rendimiento es clave en estos dominios.. 

En este tutorial, aprenderá cómo crear un perfil de sus programas para que sean más rápidos (utilice mejor la CPU) o luz de plumas (use menos memoria). Cubriré el perfil de la CPU y la memoria, utilizando el pprof (el perfilador Go), visualizando los perfiles e incluso gráficos de llamas..

El perfilado es medir el rendimiento de su programa en varias dimensiones. Go viene con un gran soporte para la creación de perfiles y puede perfilar las siguientes dimensiones de forma inmediata:

  • una muestra del tiempo de CPU por función Y instrucción
  • una muestra de todas las asignaciones de pila
  • Apilar rastros de todos los goroutines actuales.
  • Apilar los rastros que condujeron a la creación de nuevos hilos de sistema operativo.
  • Apilar trazas que llevaron al bloqueo en primitivas de sincronización.
  • Apilar los rastros de los tenedores de mutex en conflicto.

Incluso puedes crear perfiles personalizados si quieres. El perfil de Go implica crear un archivo de perfil y luego analizarlo utilizando el pprof ir herramienta.

Cómo crear archivos de perfil

Hay varias formas de crear un archivo de perfil.

Usando "go test" para generar archivos de perfil

La forma más fácil es usar ir a prueba. Tiene varias banderas que te permiten crear archivos de perfil. Aquí es cómo generar tanto un archivo de perfil de CPU como un archivo de perfil de memoria para la prueba en el directorio actual: ir prueba -cpuprofile cpu.prof -memprofile mem.prof -bench .

Descargar datos de perfil en vivo desde un servicio de larga duración

Si desea crear un perfil de un servicio web de larga duración, puede utilizar la interfaz HTTP incorporada para proporcionar datos de perfil. Agregue en algún lugar la siguiente declaración de importación:

importar _ "net / http / pprof"

Ahora, puede descargar datos de perfil en vivo desde el / debug / pprof / URL Más información está disponible en la documentación del paquete net / http / pprof.

Perfilando en Código

También puede agregar perfiles directos en su código para un control completo. Primero necesitas importar runtime / pprof. El perfil de la CPU está controlado por dos llamadas:

  • pprof.StartCPUProfile ()
  • pprof.StopCPUProfile ()

El perfil de la memoria se realiza llamando runtime.GC () seguido por pprof.WriteHeapProfile ().

Todas las funciones de creación de perfiles aceptan un identificador de archivo que usted es responsable de abrir y cerrar adecuadamente.

El programa de muestra

Para ver el perfilador en acción, usaré un programa que resuelva el Problema 8. del Proyecto Euler. El problema es: dado un número de 1,000 dígitos, encuentre los 13 dígitos adyacentes dentro de este número que tienen el producto más grande. 

Aquí hay una solución trivial que itera sobre todas las secuencias de 13 dígitos, y por cada secuencia de este tipo multiplica los 13 dígitos y devuelve el resultado. El mayor resultado es almacenado y finalmente devuelto:

paquete importación trivial ("cadenas") func calcProduct (serie) int64 dígitos: = make ([] int64, len (series)) para i, c: = rango series dígitos [i] = int64 (c) - 48  producto: = int64 (1) para i: = 0; yo < len(digits); i++  product *= digits[i]  return product  func FindLargestProduct(text string) int64  text = strings.Replace(text, "\n", "", -1) largestProduct := int64(0) for i := 0; i < len(text); i++  end := i + 13 if end > len (text) end = len (text) series: = text [i: end] resultado: = calcProduct (series) si resultado> maximumProduct primoProducto = resultado devolver el producto más grande 

Más adelante, después del perfilado, veremos algunas formas de mejorar el rendimiento con otra solución..

Perfil de CPU

Vamos a perfilar la CPU de nuestro programa. Usaré el método de prueba de go usando esta prueba:

importación ( "prueba") de texto const = '73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 85861560789112949495459501737958331952853208805511 12540698747158523863050715693290963295227443043557 66896648950445244523161731856403098711121722383113 62229893423380308135336276614282806444486645238749 30358907296290491560440772390713810515859307960866 70172427121883998797908792274921901699720888093776 65727333001053367881220235421809751254540594752243 52584907711670556013604839586446706324415722155397 53697817977846174064955149290862569321978468622482 83972241375657056057490261407972968652414535100474 82166370484403199890008895243450658541227588666881 16427171479924442928230863465674813919123162824586 17866458359124566529476545682848912883142607690042 24219022671055626321111109370544217506941658960408 07198403850962455444362981230987879927244284909188 84580156166097919133875499200524063689912560717606 0588611646710940507754100225698315520005593572 9725 71636269561882670428252483600823257530420752963450 'func TestFindLargestProduct (t * testing.T) para i: = 0; yo < 100000; i++  res := FindLargestProduct(text) expected := int64(23514624000) if res != expected  t.Errorf("Wrong!")    

Tenga en cuenta que ejecuto la prueba 100,000 veces porque el generador de perfiles go es un generador de perfiles de muestreo que necesita el código para pasar realmente un tiempo significativo (varios milisegundos acumulados) en cada línea de código. Aquí está el comando para preparar el perfil:

ve a prueba -cpuprofile cpu.prof -bench. ok _ / github.com / the-gigi / project-euler / 8 / go / trivial 13.243s 

Tomó un poco más de 13 segundos (por 100,000 iteraciones). Ahora, para ver el perfil, use la herramienta pprof go para ingresar al indicador interactivo. Hay muchos comandos y opciones. El comando más básico es topN; con la opción -cum muestra las funciones N principales que tardaron más tiempo en acumularse (por lo tanto, una función que toma muy poco tiempo en ejecutarse, pero se llama muchas veces, puede estar en la parte superior). Esto es usualmente con lo que comienzo.

> go tool pprof cpu.prof Tipo: cpu Hora: 23 de octubre de 2017 a las 8:05 am (PDT) Duración: 13.22s, Total de muestras = 13.10s (99.06%) Ingresando al modo interactivo (escriba "ayuda" para los comandos) (pprof ) top5 -cum Mostrando nodos que representan 1.23s, 9.39% del total de 13.10s Dropped 76 nodes (cum <= 0.07s) Showing top 5 nodes out of 53 flat flat% sum% cum cum% 0.07s 0.53% 0.53% 10.64s 81.22% FindLargestProduct 0 0% 0.53% 10.64s 81.22% TestFindLargestProduct 0 0% 0.53% 10.64s 81.22% testing.tRunner 1.07s 8.17% 8.70% 10.54s 80.46% trivial.calcProduct 0.09s 0.69% 9.39% 9.47s 72.29% runtime.makeslice 

Vamos a entender la salida. Cada fila representa una función. Elidí la ruta a cada función debido a restricciones de espacio, pero se mostrará en la salida real como la última columna. 

Plano significa el tiempo (o porcentaje) gastado dentro de la función, y Cum significa acumulativo: el tiempo gastado dentro de la función y todas las funciones a las que llama. En este caso, testing.tRunner en realidad llama TestFindLargestProduct (), que llama FindLargestProduct (), pero como prácticamente no se gasta tiempo allí, el perfilador de muestreo cuenta su tiempo plano como 0.

Perfil de memoria

El perfil de memoria es similar, excepto que creas un perfil de memoria:

ir prueba -memprofile mem.prof -bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / trivial

Puedes analizar el uso de tu memoria usando la misma herramienta..

Usando pprof para optimizar la velocidad de tu programa

Veamos qué podemos hacer para resolver el problema más rápido. Mirando el perfil, vemos que calcProducto () Toma 8.17% del tiempo de ejecución plano, pero makeSlice (), que se llama desde calcProducto (), está tomando 72% (acumulativo porque llama a otras funciones). Esto da una buena indicación de lo que necesitamos optimizar. ¿Qué hace el código? Para cada secuencia de 13 números adyacentes, asigna una porción:

func calcProduct (serie string) int64 dígitos: = make ([] int64, len (series))… 

Eso es casi 1,000 veces por carrera, y corremos 100,000 veces. Las asignaciones de memoria son lentas. En este caso, realmente no hay necesidad de asignar un nuevo segmento cada vez. En realidad, no es necesario asignar ninguna porción en absoluto. Podemos simplemente escanear la matriz de entrada. 

El siguiente fragmento de código muestra cómo calcular el producto en ejecución simplemente dividiendo por el primer dígito de la secuencia anterior y multiplicando por el canalla dígito. 

if cur == 1 currProduct / = old continue if old == 1 currProduct * = cur else currProduct = currProduct / old * cur if currProduct> largerProduct biggestProduct = currProduct 

Aquí hay una breve lista de algunas de las optimizaciones algorítmicas:

  • Cálculo de un producto en ejecución. Supongamos que calculamos el producto en el índice N ... N + 13 y lo llamamos P (N). Ahora necesitamos calcular el producto en el índice N + 1 ... N + 13. P (N + 1) es igual a P (N), excepto que el primer número en el índice N desapareció y debemos tener en cuenta el nuevo número en el índice N + 14T. Esto se puede hacer dividiendo el producto anterior por su primer número y multiplicando por el nuevo número. 
  • No computar ninguna secuencia de 13 números que contengan 0 (el producto siempre será cero).
  • Evitando la división o multiplicación por 1..

El programa completo está aquí. Hay una lógica espinosa para trabajar alrededor de los ceros, pero aparte de eso es bastante sencillo. Lo principal es que solo asignamos una matriz de 1000 bytes al principio, y la pasamos por un puntero (por lo que no hay copia) a la  findLargestProductInSeries () función con un rango de índices.

función de exploración de paquetes findLargestProductInSeries (dígitos * [1000] byte, start, end int) int64 if (end - start) < 13  return -1  largestProduct := int64((*digits)[start]) for i := 1; i < 13 ; i++  d := int64((*digits)[start + i]) if d == 1  continue  largestProduct *= d  currProduct := largestProduct for ii := start + 13; ii < end; ii++  old := int64((*digits)[ii-13]) cur := int64((*digits)[ii]) if old == cur  continue  if cur == 1  currProduct /= old continue  if old == 1  currProduct *= cur  else  currProduct = currProduct / old * cur  if currProduct > largerProduct largerProduct = currProduct devolver largerProduct func FindLargestProduct (cadena de texto) int64 var dígitos [1000] byte digIndex: = 0 para _, c: = rango de texto si c == 10 continuar dígitos [digIndex] = byte (c) - 48 digIndex ++ inicio: = -1 final: = -1 findStart: = verdadero var más grandeProducto int64 para ii: = 0; ii < len(digits) - 13; ii++  if findStart  if digits[ii] == 0  continue  else  start = ii findStart = false   if digits[ii] == 0  end = ii result := findLargestProductInSeries(&digits, start, end) if result > largerProduct biggestProduct = result findStart = true devolver largerProduct

La prueba es la misma. Veamos cómo lo hicimos con el perfil:

> ir prueba -cpuprofile cpu.prof -bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / scan 0.816s 

Desde el principio, podemos ver que el tiempo de ejecución se redujo de más de 13 segundos a menos de un segundo. Eso es bastante bueno. Es hora de mirar dentro. Vamos a usar solo top10, que ordena por tiempo plano.

(pprof) top10 Mostrando nodos que representan 560ms, 100% de 560ms total% plano% suma% cum cum% 290ms 51.79% 51.79% 290ms 51.79% findLargestProductInSeries 250ms 44.64% 96.43% 540ms 96.43% FindLargestProduct 20ms 3.57% 100% 20ms .usleep 0 0% 100% 540ms 96.43% TestFindLargestProduct 0 0% 100% 20ms 3.57% runtime.mstart 0 0% 100% 20ms 3.57% runtime.mstart1 0 0% 100% 20ms 3.57% runtime.sysmon 0 0% 100% 540ms 96.43% testing.tRunner 

Esto es genial. Casi todo el tiempo de ejecución se gasta dentro de nuestro código. No hay asignaciones de memoria en absoluto. Podemos profundizar más y observar el nivel de la declaración con el comando list:

(pprof) lista FindLargestProduct Total: 560ms ROUTINE ======================== scan.FindLargestProduct 250ms 540ms (plano, cum) 96.43% del total ... 44: … 45:… 46: func FindLargestProduct (t string) int64 … 47: var dígitos [1000] byte… 48: digIndex: = 0 70ms 70ms 49: para _, c: = rango de texto … 50: if c == 10 … 51: continuar… 52:… 53: dígitos [digIndex] = byte (c) - 48 10ms 10ms 54: digIndex ++… 55:… 56:… 57: inicio: = -1… 58: final: = -1 ... 59: findStart: = verdadero ... 60: var más grandeProducto int64 ... 61: para ii: = 0; ii < len(digits)-13; ii++  10ms 10ms 62: if findStart … 63: if digits[ii] == 0 … 64: continue… 65:  else … 66: start = ii… 67: findStart = false… 68: … 69: … 70: 70ms 70ms 71: if digits[ii] == 0 … 72: end = ii 20ms 310ms 73: result := f(&digits,start,end) 70ms 70ms 74: if result > El producto más grande … 75: El producto más grande = resultado ... 76: ... 77: findStart = verdadero ... 78: ... 79:

Esto es bastante sorprendente. Obtienes una declaración por cada momento de todos los puntos importantes. Tenga en cuenta que la llamada en la línea 73 a función f () es en realidad una llamada a findLargestProductInSeries (), que renombré en el perfil debido a limitaciones de espacio. Esta llamada tarda 20 ms. Tal vez, al integrar el código de función en su lugar, podemos guardar la llamada de función (incluida la asignación de pila y copiar argumentos) y guardar esos 20 ms. Puede haber otras optimizaciones valiosas que esta vista puede ayudar a identificar.

Visualización

Mirar estos perfiles de texto puede ser difícil para programas grandes. Go te da muchas opciones de visualización. Tendrás que instalar Graphviz para la siguiente sección.

La herramienta pprof puede generar resultados en muchos formatos. Una de las maneras más fáciles (svg output) es simplemente escribir 'web' desde el indicador interactivo de pprof, y su navegador mostrará un gráfico agradable con el camino marcado marcado en rosa.

Gráficos de llamas

Los gráficos incorporados son agradables y útiles, pero con programas grandes, incluso estos gráficos pueden ser difíciles de explorar. Una de las herramientas más populares para visualizar los resultados de rendimiento es el gráfico de llama. La herramienta pprof todavía no la admite, pero puedes jugar con gráficos de llamas que ya utilizan la herramienta de antorcha de Uber. Hay trabajo en curso para agregar soporte incorporado para gráficos de llama a pprof.

Conclusión

Go es un lenguaje de programación de sistemas que se utiliza para construir sistemas distribuidos de alto rendimiento y almacenes de datos. Go viene con un excelente soporte que sigue mejorando para perfilar sus programas, analizar su rendimiento y visualizar los resultados.. 

El equipo de Go y la comunidad ponen mucho énfasis en mejorar las herramientas en torno al rendimiento. El código fuente completo con tres algoritmos diferentes se puede encontrar en GitHub.