Hola Mi nombre es Marco, trabajo para Badoo en el departamento de Plataforma. Tenemos muchas cosas escritas en Go, y a menudo son cr铆ticas para el rendimiento del sistema. Es por eso que hoy te ofrezco una traducci贸n de un art铆culo que realmente me gust贸 y, estoy seguro, te ser谩 muy 煤til. El autor muestra paso a paso c贸mo abord贸 los problemas de rendimiento y c贸mo los resolvieron. Incluyendo que se familiarizar谩 con las ricas herramientas disponibles en Go para dicho trabajo. Que tengas una buena lectura!Hace unas semanas, le铆 el art铆culo "
C贸digo bueno contra c贸digo malo en Go "
, donde el autor, paso a paso, demuestra la refactorizaci贸n de una aplicaci贸n real que resuelve problemas comerciales reales. Se enfoca en convertir el "c贸digo malo" en "c贸digo bueno": m谩s idiom谩tico, m谩s comprensible, utilizando plenamente los detalles de Go. Pero el autor tambi茅n declar贸 la importancia del rendimiento de la aplicaci贸n en cuesti贸n. La curiosidad se apoder贸 de m铆: 隆intentemos acelerarlo!
El programa, en t茅rminos generales, lee el archivo de entrada, lo analiza l铆nea por l铆nea y llena los objetos en la memoria.

El autor no solo public贸 el
c贸digo fuente en GitHub , sino que tambi茅n escribi贸 un punto de referencia. Esta es una gran idea De hecho, el autor invit贸 a todos a jugar con el c贸digo y tratar de acelerarlo. Para reproducir los resultados del autor, use el siguiente comando:
$ go test -bench=.
渭s por llamada (menos - mejor)Resulta que en mi computadora el "buen c贸digo" es un 16% m谩s r谩pido. 驴Podemos acelerarlo?
En mi experiencia, existe una correlaci贸n entre la calidad del c贸digo y el rendimiento. Si refactoriz贸 con 茅xito el c贸digo, lo hizo m谩s limpio y menos conectado, probablemente lo hizo m谩s r谩pido porque se volvi贸 menos abarrotado (y no hay m谩s instrucciones innecesarias que se ejecutaron previamente en vano). Quiz谩s durante la refactorizaci贸n not贸 algunas oportunidades de optimizaci贸n, o ahora solo tiene la oportunidad de aprovecharlas. Pero, por otro lado, si desea que el c贸digo sea a煤n m谩s productivo, probablemente tenga que alejarse de la simplicidad y agregar varios hacks. Realmente ahorras milisegundos, pero la calidad del c贸digo se ver谩 afectada: ser谩 m谩s dif铆cil leerlo y hablar sobre 茅l, se volver谩 m谩s fr谩gil y flexible.
Subimos la monta帽a de la simplicidad, y luego bajamos de ellaEsto es una compensaci贸n: 驴hasta d贸nde est谩s dispuesto a llegar?
Para priorizar adecuadamente el trabajo de aceleraci贸n, la estrategia 贸ptima es encontrar cuellos de botella y concentrarse en ellos. Para hacer esto, use las herramientas de creaci贸n de perfiles.
pprof y
trace son tus amigos:
$ go test -bench=. -cpuprofile cpu.prof $ go tool pprof -svg cpu.prof > cpu.svg
Un gr谩fico bastante grande del uso de la CPU (haga clic para SVG) $ go test -bench=. -trace trace.out $ go tool trace trace.out
Seguimiento del arco iris: muchas tareas peque帽as (haga clic para abrir, solo funciona en Google Chrome)El seguimiento confirma que todos los n煤cleos de procesador est谩n ocupados (l铆neas por debajo de 0, 1, etc.) y, a primera vista, esto es bueno. Pero tambi茅n muestra miles de peque帽os "c谩lculos" de colores y varias 谩reas vac铆as donde los n煤cleos estaban inactivos. Vamos a acercarnos:
"Ventana" en 3 ms (haga clic para abrir, solo funciona en Google Chrome)Cada n煤cleo est谩 inactivo durante bastante tiempo y tambi茅n "salta" entre micro tareas todo el tiempo. Parece que la granularidad de estas tareas no es muy 贸ptima, lo que lleva a una gran cantidad de
cambios de
contexto y a la competencia debido a la sincronizaci贸n.
Veamos qu茅 nos dice el
detector de vuelo . 驴Hay alg煤n problema en el acceso s铆ncrono a los datos (si hay alguno, entonces tenemos problemas mucho mayores que el rendimiento)?
$ go test -race PASS
Genial Todo esta correcto. No se encontraron vuelos. Las funciones de prueba y las funciones de referencia son funciones diferentes (
consulte la documentaci贸n ), pero aqu铆 llaman a la misma funci贸n
ParseAdexpMessage , por lo que lo que verificamos para vuelos de datos por pruebas est谩 bien.
El modelo competitivo en la versi贸n "buena" consiste en procesar cada l铆nea desde el archivo de entrada en una rutina diferente (para usar todos los n煤cleos). La intuici贸n del autor aqu铆 funcion贸 bien, ya que las gorutinas tienen fama de caracter铆sticas f谩ciles y baratas. Pero, 驴cu谩nto ganamos con la ejecuci贸n paralela? Comparemos con el mismo c贸digo pero sin usar goroutines (solo elimine la palabra go que viene antes de la llamada a la funci贸n):


Vaya, parece que el c贸digo se ha vuelto m谩s r谩pido sin usar concurrencia. Esto significa que la sobrecarga (distinta de cero) para lanzar gorutinas excede el tiempo que ganamos al usar varios n煤cleos al mismo tiempo. El siguiente paso natural deber铆a ser eliminar la sobrecarga (que no sea cero) para usar canales para enviar los resultados. Vamos a reemplazarlo con un corte regular:
渭s por llamada (menos es mejor)Obtuvimos aproximadamente un 40% de aceleraci贸n en comparaci贸n con la versi贸n "buena", simplificando el c贸digo y eliminando la competencia (
diff ).
Con una gorutina, solo un n煤cleo funciona a la vezVeamos ahora las funciones activas en el gr谩fico pprof:
Buscando cuellos de botellaEl punto de referencia de la versi贸n actual (operaci贸n secuencial, cortes) pasa el 86% del tiempo analizando mensajes, y esto es normal. Pero notaremos r谩pidamente que el 43% del tiempo se gasta en usar expresiones regulares y la funci贸n
(* Regexp) .FindAll .
A pesar de que las expresiones regulares son una forma conveniente y flexible de obtener datos de texto plano, tienen inconvenientes, incluido el uso de una gran cantidad de recursos y un procesador y memoria. Son una herramienta poderosa, pero a menudo su uso es innecesario.
En nuestro programa, una plantilla
patternSubfield = "-.[^-]*"
Su objetivo principal es resaltar los comandos que comienzan con un gui贸n (-), y puede haber varios en la l铆nea. Esto, despu茅s de haber extra铆do un peque帽o c贸digo, se puede hacer usando
bytes . Adaptemos el c贸digo (
commit ,
commit ) para cambiar las expresiones regulares a Split:
渭s por llamada (menos es
mejor)Wow! 隆C贸digo 40% m谩s productivo! El gr谩fico de consumo de CPU ahora se ve as铆:

No m谩s tiempo perdido en expresiones regulares. Una parte importante (40%) se destina a la asignaci贸n de memoria de cinco funciones diferentes. Curiosamente, ahora el 21% del tiempo se dedica a los
bytes. Funci贸n de
recorte :
Esta caracter铆stica me intriga. 驴Qu茅 podemos hacer aqu铆?
bytes.Trim espera una cadena con caracteres que "corta" como argumento, pero como esta cadena pasamos una cadena con un solo car谩cter: un espacio. Este es solo un ejemplo de c贸mo puede obtener la aceleraci贸n debido a la complejidad: creemos nuestra funci贸n de recorte en lugar de la est谩ndar. Nuestra funci贸n de
recorte personalizado funcionar谩 con un solo byte en lugar de una l铆nea completa:

渭s por llamada (menos es mejor)隆Hurra, otro 20% de descuento! La versi贸n actual es cuatro veces m谩s r谩pida que la original "mala" y al mismo tiempo usa solo un n煤cleo. No esta mal!
Anteriormente, abandonamos la competitividad en el nivel de procesamiento de l铆nea, pero sostengo que la aceleraci贸n se puede lograr utilizando la competitividad en un nivel superior. Por ejemplo, procesar 6,000 archivos (6,000 mensajes) es m谩s r谩pido en mi computadora si cada archivo se procesa en su propia rutina:
渭s por llamada (menos es mejor; el morado es una soluci贸n competitiva)La ganancia es del 66% (es decir, aceleraci贸n tres veces). Esto es bueno, pero no mucho, teniendo en cuenta que se utilizan los 12 n煤cleos de procesador que tengo. Esto puede significar que el nuevo c贸digo optimizado que procesa todo el archivo sigue siendo una "peque帽a tarea", para la cual la sobrecarga para crear gorutinas y el costo de sincronizaci贸n no son insignificantes. Curiosamente, aumentar el n煤mero de mensajes de 6,000 a 120,000 no tiene ning煤n efecto en la versi贸n de subproceso 煤nico y reduce el rendimiento en la versi贸n de "una rutina por mensaje". Esto se debe a que, a pesar del hecho de que crear una cantidad tan grande de gorutinas es posible y a veces 煤til, trae su propia sobrecarga en el 谩rea del
programador de tiempo de
ejecuci贸n .
Podemos reducir a煤n m谩s el tiempo de ejecuci贸n (no 12 veces, pero a煤n as铆) creando solo unos pocos trabajadores. Por ejemplo, 12 gorutinas de larga vida, cada una de las cuales procesar谩 parte de los mensajes:
渭s por llamada (menos es mejor; el morado es una soluci贸n competitiva)Esta opci贸n reduce el tiempo de ejecuci贸n en un 79% en comparaci贸n con la versi贸n de subproceso 煤nico. Tenga en cuenta que esta estrategia solo tiene sentido si tiene muchos archivos para procesar.
El uso 贸ptimo de todos los n煤cleos de procesador es usar varias gorutinas, cada una de las cuales procesa una cantidad significativa de datos sin ninguna interacci贸n o sincronizaci贸n antes de que se complete el trabajo.
Por lo general, toman tantos procesos (goroutine) como los n煤cleos del procesador, pero esta no siempre es la mejor opci贸n: todo depende de la tarea espec铆fica. Por ejemplo, si est谩 leyendo algo del sistema de archivos o haciendo muchas llamadas de red, para obtener m谩s rendimiento, debe usar m谩s goroutines que sus n煤cleos.
渭s por llamada (menos es mejor; el morado es una soluci贸n competitiva)Hemos llegado al punto en que el rendimiento del an谩lisis es dif铆cil de aumentar con algunos cambios localizados. El tiempo de ejecuci贸n est谩 dominado por el tiempo para la asignaci贸n de memoria y la recolecci贸n de elementos no utilizados. Esto suena l贸gico ya que las funciones de administraci贸n de memoria son bastante lentas. Una mayor optimizaci贸n de los procesos asociados con las asignaciones sigue siendo una tarea para los lectores.
El uso de otros algoritmos tambi茅n puede conducir a una gran ganancia de rendimiento.
Aqu铆 me inspir贸 una conferencia de Lexical Scanning en Go de Rob Pike,
para crear un lexer personalizado (
fuente ) y un analizador personalizado (
fuente ). Este c贸digo a煤n no est谩 listo (no proceso un mont贸n de casos de esquina), es menos claro que el algoritmo original y, a veces, es dif铆cil escribir el manejo correcto de errores. Pero es peque帽o y 30% m谩s r谩pido que la versi贸n m谩s optimizada.
渭s por llamada (menos es mejor; el morado es una soluci贸n competitiva)Si Como resultado, obtuvimos una aceleraci贸n de 23 veces en comparaci贸n con el c贸digo fuente.
Eso es todo por hoy. Espero que hayas disfrutado esta aventura. Aqu铆 hay algunas notas y conclusiones:
- La productividad se puede mejorar en varios niveles de abstracci贸n, utilizando diferentes t茅cnicas, y la ganancia a menudo se incrementa.
- El ajuste debe comenzar con abstracciones de alto nivel: estructuras de datos, algoritmos, el desacoplamiento correcto de los m贸dulos. Tome abstracciones de bajo nivel m谩s adelante: E / S, procesamiento por lotes, competitividad, uso de la biblioteca est谩ndar, trabajo con memoria, asignaci贸n de memoria.
- El an谩lisis Big O es muy importante, pero generalmente no es la herramienta m谩s adecuada para acelerar un programa.
- Escribir puntos de referencia es un trabajo duro. Use perfiles y puntos de referencia para encontrar cuellos de botella y obtener una comprensi贸n m谩s amplia de lo que est谩 sucediendo en el programa. Tenga en cuenta que los resultados de referencia no son los mismos que sus usuarios experimentar谩n en el trabajo de la vida real.
- Afortunadamente, un conjunto de herramientas ( Bench , pprof , trace , Race Detector , Cover ) hace que la investigaci贸n sobre el rendimiento del c贸digo sea asequible e interesante.
- Escribir buenas pruebas relevantes no es una tarea trivial. Pero son muy importantes para no ir a la naturaleza. Puede refactorizar, asegur谩ndose de que el c贸digo siga siendo correcto.
- Detente y preg煤ntate qu茅 tan r谩pido es "lo suficientemente r谩pido". No pierdas tu tiempo optimizando algunos guiones 煤nicos. No olvide que la optimizaci贸n no es gratuita: el tiempo, la complejidad, los errores y la deuda t茅cnica del ingeniero.
- Pi茅nselo dos veces antes de complicar el c贸digo.
- Los algoritmos con complejidad 惟 (n虏) y superiores suelen ser demasiado caros.
- Los algoritmos con complejidad O (n) u O (n log n) y a continuaci贸n generalmente est谩n bien.
- Varios factores ocultos no pueden ser ignorados. Por ejemplo, todas las mejoras en el art铆culo se realizaron al reducir estos factores y no al cambiar la clase de complejidad del algoritmo.
- La E / S suele ser un cuello de botella: consultas de red, consultas de bases de datos, sistema de archivos.
- Las expresiones regulares son a menudo demasiado caras e innecesarias.
- Las asignaciones de memoria son m谩s caras que los c谩lculos.
- Un objeto asignado en la pila es m谩s barato que un objeto asignado en el mont贸n.
- Los cortes son 煤tiles como una alternativa a los costosos movimientos de memoria.
- Las cadenas son efectivas cuando son de solo lectura (incluida la reorganizaci贸n). En todos los dem谩s casos, [] byte son m谩s efectivos.
- Es muy importante que los datos que procesa est茅n cerca (cach茅s del procesador).
- La competitividad y el paralelismo son muy 煤tiles, pero dif铆ciles de preparar.
- Cuando caves profundo y bajo, recuerda el "piso de vidrio" en el que no quieres entrar. Si sus manos est谩n ansiosas por probar las instrucciones del ensamblador, las instrucciones SIMD, es posible que necesite usar Go solo para la creaci贸n de prototipos y luego cambiar a un idioma de nivel inferior para obtener el control total del hardware y cada nanosegundo.