¿Cómo funcionan los perfiladores en Ruby y Python?

La traducción del artículo se preparó antes del inicio del curso avanzado "Python Developer" .



El artículo original se puede leer aquí .










¡Hola! Como aperitivo para el generador de perfiles Ruby, quería hablar sobre cómo funcionan los perfiladores Ruby y Python existentes. También ayudará a responder la pregunta que mucha gente me hace: "¿Cómo escribir un perfilador?"



En este artículo, nos centraremos en los perfiladores de procesadores (y no, digamos, en los perfiladores de memoria / montón). Cubriré algunos enfoques básicos para escribir un generador de perfiles, proporcionaré algunos ejemplos de código y mostraré muchos ejemplos de generadores de perfiles populares en Ruby y Python, y también le mostraré cómo funcionan bajo el capó.



Probablemente, puede haber errores en el artículo (en preparación para escribirlo, revisé parcialmente el código de 14 bibliotecas para crear perfiles y no estaba familiarizado con muchas de ellas hasta ahora), así que avíseme si las encuentra. ...



2 tipos de perfiladores



Hay dos tipos principales de perfiladores de procesador: perfiladores de muestreo y de seguimiento .



Los perfiladores de seguimiento registran todas las llamadas a funciones en su programa y, en última instancia, proporcionan un informe. Los perfiladores de muestreo adoptan un enfoque estadístico, escriben la pila cada pocos milisegundos y generan un informe basado en estos datos.



La razón principal para usar un generador de perfiles de muestreo en lugar de un generador de perfiles de seguimiento es porque es liviano. Toma 20 o 200 fotografías por segundo, no lleva mucho tiempo. Dichos perfiladores serán muy efectivos si tiene un problema grave de rendimiento (el 80% del tiempo se dedica a llamar a una función lenta), ya que 200 instantáneas por segundo serán suficientes para identificar la función problemática.



Perfiladores



A continuación, daré un resumen general de los perfiladores discutidos en este artículo ( desde aquí ). Explicaré los términos utilizados en este artículo ( setitimer , rb_add_event_hook , ptrace ) un poco más tarde. Curiosamente, todos los perfiladores se implementan utilizando un pequeño conjunto de características básicas.



Perfiladores de Python







"Gdb hacks" no es realmente un generador de perfiles de Python, sino que enlaza con un sitio web que explica cómo implementar un generador de perfiles de piratas informáticos como un envoltorio de script de shell alrededor de gdb . Se trata específicamente de Python, ya que las versiones más nuevas de gbd implementarán la pila de Python por usted. Algo así como pyflame para los pobres.



Perfiladores de rubí







Casi todos estos perfiladores viven dentro de su proceso.



Antes de entrar en los detalles de estos perfiladores, hay una cosa muy importante: todos estos perfiladores, excepto pyflame , se ejecutan dentro de su proceso Python / Ruby. Si está dentro de un programa Python / Ruby, generalmente tiene fácil acceso a la pila. Por ejemplo, aquí hay un programa Python simple que imprime el contenido de la pila de cada hilo en ejecución:



import sys
import traceback

def bar():
    foo()

def foo():
    for _, frame in sys._current_frames().items():
        for line in traceback.extract_stack(frame):
            print line

bar()


Aquí está la salida de la consola. Puede ver que tiene nombres de funciones de la pila, números de línea, nombres de archivos, todo lo que pueda necesitar si está perfilando.



('test2.py', 12, '<module>', 'bar()')
('test2.py', 5, 'bar', 'foo()')
('test2.py', 9, 'foo', 'for line in traceback.extract_stack(frame):')


Es incluso más simple en Ruby: puede usar pone llamadas para obtener la pila.



La mayoría de estos perfiladores son extensiones de rendimiento de C, por lo que difieren ligeramente, pero estas extensiones para programas Ruby / Python también tienen fácil acceso a la pila de llamadas.



Cómo funcionan los perfiladores de rastreo



He enumerado todos los perfiles de seguimiento de Ruby y Python en las tablas anteriores: rblineprof, ruby-prof, line_profiler y cProfile . Todos funcionan de forma similar. Registran todas las llamadas a funciones y son extensiones C para reducir los gastos generales.



¿Cómo trabajan? Tanto en Ruby como en Python, puede especificar una devolución de llamada que se activa cuando ocurren varios eventos de intérprete (por ejemplo, "una llamada de función" o "ejecución de línea de código"). Cuando se llama a la devolución de llamada, escribe la pila para un análisis posterior.



Es útil ver exactamente dónde están estas devoluciones de llamada en el código, por lo que vincularé las líneas de código relevantes en github.



En Python, puede personalizar la devolución de llamada con PyEval_SetTraceo PyEval_SetProfile. Esto se describe en la sección de documentación.Creación de perfiles y seguimiento en Python. Dice, " PyEval_SetTracesimilar a PyEval_SetProfileexcepto que la función de seguimiento recibe eventos de número de línea".



El código:



  • line_profilerconfigura su devolución de llamada usando PyEval_SetTrace: ver line_profiler.pyxlínea 157
  • cProfileconfigura su devolución de llamada usando PyEval_SetProfile: vea la _lsprof.c línea 693 (cProfile se implementa usando lsprof )


En Ruby, puede personalizar su devolución de llamada con rb_add_event_hook. No pude encontrar ninguna documentación al respecto, pero así es como se ve.



rb_add_event_hook(prof_event_hook,
      RUBY_EVENT_CALL | RUBY_EVENT_RETURN |
      RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN |
      RUBY_EVENT_LINE, self);


Firma prof_event_hook:



static void
prof_event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)




Algo como PyEval_SetTraceen Python, pero de una forma más flexible: puede elegir los eventos sobre los que desea recibir notificaciones (por ejemplo, "solo llamadas a funciones").



El código:



  • ruby-prof rb_add_event_hook : ruby-prof.c 329
  • rblineprof rb_add_event_hook : rblineprof.c 649


tracing-



La principal desventaja de los perfiladores de rastreo implementados de esta manera es que agregan una cantidad fija de código para cada función / llamada de línea que se ejecuta. ¡Puede hacerte tomar decisiones equivocadas! Por ejemplo, si tiene dos implementaciones de algo, una con muchas llamadas a funciones y la otra sin, que generalmente toman la misma cantidad de tiempo, entonces la primera con muchas llamadas a funciones parecerá más lenta al generar perfiles.



Para ilustrar, creé un pequeño archivo llamado test.pycon el siguiente contenido y comparé el tiempo de ejecución python -mcProfile test.pyy python test.py. python. test.pycompletado en aproximadamente 0,6 sy en python -mcProfile test.pyaproximadamente 1 s. Entonces, para este ejemplo en particular, cProfileagregué una sobrecarga adicional de ~ 60%.

La documentación cProfiledice:

La naturaleza interpretada de Python agrega tanta sobrecarga de tiempo de ejecución que la creación de perfiles deterministas tiende a agregar una pequeña sobrecarga de procesamiento en aplicaciones normales.


Esto parece una afirmación bastante razonable: el ejemplo anterior (que hace 3,5 millones de llamadas a funciones y nada más) obviamente no es un programa Python normal, y casi cualquier otro programa tendrá menos gastos generales.

No he comprobado el ruby-profgenerador de perfiles de seguimiento de Ruby, pero su README dice lo siguiente:

La mayoría de los programas se ejecutarán aproximadamente la mitad de lento, mientras que los programas altamente recursivos (como la prueba de la serie Fibonacci) se ejecutarán tres veces más lento .


Cómo funcionan generalmente los perfiladores de muestreo: setitimer



Es hora de hablar sobre el segundo tipo de perfilador: ¡perfiladores de muestreo!

La mayoría de los perfiladores de muestreo en Ruby y Python se implementan mediante una llamada al sistema setitimer. ¿Lo que es?



Supongamos que desea tomar una instantánea de la pila de programas 50 veces por segundo. Esto puede hacerse de la siguiente manera:



  • Pídale al kernel de Linux que le envíe una señal cada 20 milisegundos (usando una llamada al sistema setitimer);
  • Registre un manejador de señales para una instantánea de la pila cuando se reciba una señal;
  • Cuando se complete la creación de perfiles, solicite a Linux que deje de enviarle señales y proporcione el resultado.


Si desea ver un caso de uso práctico setitimerpara implementar un generador de perfiles de muestreo, creo que el stacksampler.pymejor ejemplo es un generador de perfiles útil y funcional, y tiene unas 100 líneas en Python. ¡Y esto es genial!



La razón por la que stacksampler.pysolo toma 100 líneas en Python es que cuando registra una función de Python como manejador de señales, la función se pasa a la pila actual de su programa. Por lo tanto, stacksampler.pyes muy fácil registrar un manejador de señales :



def _sample(self, signum, frame):
   stack = []
   while frame is not None:
       stack.append(self._format_frame(frame))
       frame = frame.f_back

   stack = ';'.join(reversed(stack))
   self._stack_counts[stack] += 1


Simplemente saca una pila de un marco e incrementa el número de veces que se ha visto una pila en particular. ¡Tan sencillo! ¡Muy guay!



Echemos un vistazo a todos nuestros otros perfiladores que usan setitimery averigüemos en qué parte del código llaman setitimer:



  • stackprof (Ruby): stackprof.c 118
  • perftools.rb (Ruby): , , , , gem (?)
  • stacksampler (Python): stacksampler.py 51
  • statprof (Python): statprof.py 239
  • vmprof (Python): vmprof_unix.c 294


Lo importante es que setitimerdebes decidir cómo contar el tiempo. ¿Quieres 20 ms en tiempo real? 20 ms de tiempo de CPU de usuario? 20 ms de tiempo de CPU del sistema + usuario? Si observa detenidamente los enlaces anteriores, notará que estos perfiladores en realidad usan cosas diferentes setitimer: a veces el comportamiento es personalizable y otras no. La página del manual es setitimerbreve y vale la pena leerla para conocer todas las configuraciones posibles. señaló un caso de uso interesante



@mgedminen Twittersetitimer . Este problema y este problema revelan más detalles.



Un inconveniente INTERESANTE de los perfiladores basados ​​ensetitimer- ¡Qué temporizadores disparan señales! Las señales a veces interrumpen las llamadas al sistema. ¡Las llamadas al sistema a veces tardan unos milisegundos! Si toma instantáneas con demasiada frecuencia, puede hacer que el programa ejecute llamadas al sistema indefinidamente.



Perfiladores de muestreo que no utilizan setitimer



Hay varios perfiladores de muestreo que no utilizan setitimer:



  • pyinstrumentutiliza PyEval_SetProfile(por lo que es una especie de generador de perfiles de seguimiento), pero no siempre recopila instantáneas de la pila cuando se llama a la devolución de llamada de seguimiento. Aquí está el código que selecciona el momento de la instantánea de seguimiento de pila . Lea más sobre esta solución en este blog . (básicamente: le setitimerpermite perfilar solo el hilo principal en Python)
  • pyflamePerfila el código Python fuera de un proceso mediante una llamada al sistema ptrace. Utiliza un bucle en el que toma fotografías, duerme durante un cierto tiempo y vuelve a hacer lo mismo. Aquí hay una llamada para esperar.
  • python-flamegraphadopta un enfoque similar en el que inicia un nuevo hilo en su proceso de Python y obtiene seguimientos de pila, inactividad y reintentos de nuevo. Aquí hay una llamada para esperar .


Los 3 de estos perfiladores toman instantáneas en tiempo real.



Publicaciones de blog de Pyflame



He pasado casi todo mi tiempo en esta publicación en otros perfiladores pyflame, pero en realidad me interesa más porque perfila su programa Python desde un proceso separado, por lo que quiero que mi perfilador Ruby funcione de manera similar.



Por supuesto, todo es un poco más complicado de lo que describí. No entraré en detalles, pero Evan Klitzke ha escrito muchos buenos artículos sobre esto en su blog:





Puede encontrar más información en eklitzke.org . Todas estas son cosas muy interesantes sobre las que voy a leer más de cerca; ¡quizás resulte ptracemejor que process_vm_readvimplementar un generador de perfiles Ruby! Tiene process_vm_readvmenos sobrecarga ya que no detiene el proceso, pero también puede darle una instantánea incorrecta ya que no detiene el proceso :). En mis experimentos, obtener imágenes contradictorias no fue un gran problema, pero creo que aquí realizaré una serie de experimentos.



¡Eso es todo por hoy!



Hay muchas sutilezas importantes en las que no entré en esta publicación, por ejemplo, básicamente dije que vmprofy stacksamplerson similares (no lo son)vmprofadmite la creación de perfiles de cadenas y la creación de perfiles de funciones de Python escritas en C, lo que creo que hace que el generador de perfiles sea más complejo). Pero tienen algunos de los mismos principios básicos, por lo que creo que la revisión de hoy será un buen punto de partida.






TDD con y sin pytest. Seminario web gratuito






Lee mas:






All Articles