O artigo original pode ser lido aqui .
Olá! Como aperitivo para o criador de perfis Ruby, eu gostaria de falar sobre como os criadores de perfis Ruby e Python existentes funcionam. Também ajudará a responder à pergunta que muitas pessoas me fazem: "Como escrever um perfilador?"
Neste artigo, vamos nos concentrar em profilers de processador (e não, digamos, profilers de memória / heap). Vou cobrir algumas abordagens básicas para escrever um profiler, fornecer exemplos de código e mostrar muitos exemplos de profilers populares em Ruby e Python, e também mostrar como eles funcionam nos bastidores.
Provavelmente, pode haver erros no artigo (na preparação para escrevê-lo, examinei parcialmente o código de 14 bibliotecas para criação de perfil e não estava familiarizado com muitas delas até agora), então, por favor, deixe-me saber se você os encontrar ...
2 tipos de perfiladores
Existem dois tipos principais de profilers de processador - perfiladores de amostragem e rastreamento .
Os criadores de perfil de rastreamento gravam cada chamada de função em seu programa, fornecendo, em última análise, um relatório. Os criadores de perfil de amostragem usam uma abordagem estatística, eles gravam a pilha a cada poucos milissegundos, gerando um relatório com base nesses dados.
O principal motivo para usar um perfilador de amostragem em vez de um perfilador de rastreamento é porque ele é leve. Você tira 20 ou 200 fotos por segundo - não leva muito tempo. Esses profilers serão muito eficazes se você tiver um problema sério de desempenho (80% do tempo é gasto chamando uma função lenta), já que 200 instantâneos por segundo serão suficientes para identificar a função do problema!
Perfiladores
A seguir, fornecerei um resumo geral dos criadores de perfil discutidos neste artigo (a partir daqui ). Explicarei os termos usados neste artigo ( setitimer , rb_add_event_hook , ptrace ) um pouco mais tarde. Curiosamente, todos os criadores de perfil são implementados usando um pequeno conjunto de recursos básicos.
Profilers Python
"Gdb hacks" não é realmente um criador de perfil Python - ele vincula a um site que explica como implementar um criador de perfil de hacker como um shell script wrapper em torno do gdb . Isso é especificamente sobre Python, já que as versões mais recentes do gbd irão realmente implantar a pilha Python para você. Algo como pyflame para os pobres.
Profilers Ruby
Quase todos esses profilers vivem dentro de seu processo
Antes de entrarmos nos detalhes desses profilers, há uma coisa muito importante - todos esses profilers, exceto pyflame , são executados dentro de seu processo Python / Ruby. Se você estiver dentro de um programa Python / Ruby, geralmente terá acesso fácil à pilha. Por exemplo, aqui está um programa Python simples que imprime o conteúdo da pilha de cada thread em execução:
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()
Aqui está a saída do console. Você pode ver que ele tem nomes de função da pilha, números de linha, nomes de arquivo - o que for necessário se estiver fazendo um perfil.
('test2.py', 12, '<module>', 'bar()')
('test2.py', 5, 'bar', 'foo()')
('test2.py', 9, 'foo', 'for line in traceback.extract_stack(frame):')
É ainda mais simples em Ruby: você pode usar puts caller para obter a pilha.
A maioria desses profilers são extensões de desempenho de C, então eles diferem um pouco, mas essas extensões para programas Ruby / Python também têm acesso fácil à pilha de chamadas.
Como funcionam os perfis de rastreamento
Listei todos os perfis de rastreamento Ruby e Python nas tabelas acima: rblineprof, ruby-prof, line_profiler e cProfile . Todos eles funcionam de maneira semelhante. Eles gravam cada chamada de função e são extensões C para reduzir a sobrecarga.
Como eles funcionam? Tanto em Ruby quanto em Python, você pode especificar um retorno de chamada que é disparado quando ocorrem vários eventos do interpretador (por exemplo, "uma chamada de função" ou "linha de execução de código"). Quando o retorno de chamada é chamado, ele grava a pilha para análise posterior.
É útil ver exatamente onde esses retornos de chamada estão no código, portanto, vincularei as linhas de código relevantes no github.
Em Python, você pode personalizar o retorno de chamada com
PyEval_SetTraceou PyEval_SetProfile. Isso é descrito na seção de documentaçãoCriação de perfil e rastreamento em Python. Diz " PyEval_SetTracesemelhante a, PyEval_SetProfileexceto que a função de rastreamento recebe eventos de número de linha."
O código:
line_profilerconfigura seu retorno de chamada usandoPyEval_SetTrace: verline_profiler.pyxlinha 157cProfileconfigura seu retorno de chamada usandoPyEval_SetProfile: consulte a_lsprof.clinha 693 (cProfile é implementado usando lsprof )
Em Ruby, você pode personalizar seu retorno de chamada com
rb_add_event_hook. Não consegui encontrar nenhuma documentação sobre isso, mas é assim que parece.
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);
Assinatura
prof_event_hook:
static void
prof_event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)
Algo como
PyEval_SetTraceno Python, mas em uma forma mais flexível - você pode escolher sobre quais eventos deseja ser notificado (por exemplo, "apenas chamadas de função").
O código:
tracing-
A principal desvantagem de rastrear profilers implementados dessa maneira é que eles adicionam uma quantidade fixa de código para cada chamada de função / linha que é executada. Isso pode fazer com que você tome decisões erradas! Por exemplo, se você tiver duas implementações de algo - uma com muitas chamadas de função e outra sem, o que geralmente leva a mesma quantidade de tempo, então a primeira, com muitas chamadas de função, parecerá mais lenta durante a criação de perfil.
Para ilustrar, criei um pequeno arquivo chamado
test.pycom o seguinte conteúdo e comparei o tempo de execução python -mcProfile test.pye python test.py. python. test.pyconcluído em cerca de 0,6 se em python -mcProfile test.pycerca de 1 s. Portanto, para este exemplo específico, cProfileadicionei uma sobrecarga extra de ~ 60%.
A documentaçãocProfilediz:
A natureza interpretada do Python adiciona tanta sobrecarga de tempo de execução que o perfil determinístico tende a adicionar uma pequena sobrecarga de processamento em aplicativos normais.
Esta parece ser uma declaração bastante razoável - o exemplo anterior (que faz 3,5 milhões de chamadas de função e nada mais) obviamente não é um programa Python normal e quase qualquer outro programa terá menos sobrecarga.
Eu não verifiquei a sobrecarga
ruby-prof(criador de perfil de rastreamento Ruby), mas seu README diz o seguinte:
A maioria dos programas será executada duas vezes mais devagar, enquanto os programas altamente recursivos (como o teste da série Fibonacci) serão executados três vezes mais devagar .
Como os perfis de amostragem geralmente funcionam: setitimer
É hora de falar sobre o segundo tipo de perfilador: geradores de perfil de amostragem!
A maioria dos geradores de perfil de amostragem em Ruby e Python são implementados usando uma chamada de sistema
setitimer. O que é isso?
Digamos que você queira tirar um instantâneo da pilha do programa 50 vezes por segundo. Isso pode ser feito da seguinte forma:
- Peça ao kernel do Linux para enviar um sinal a cada 20 milissegundos (usando uma chamada de sistema
setitimer); - Registre um manipulador de sinal para um instantâneo de pilha quando um sinal for recebido;
- Quando o perfil estiver concluído, peça ao Linux para parar de sinalizar para você e fornecer o resultado!
Se você quiser ver um caso de uso prático
setitimerpara implementar um criador de perfil de amostragem, acho que o stacksampler.pymelhor exemplo é um criador de perfil útil e funcional, com cerca de 100 linhas em Python. E isso é muito legal!
A razão pela qual
stacksampler.pyleva apenas 100 linhas em Python é porque quando você registra uma função Python como um manipulador de sinal, a função é passada para a pilha atual de seu programa. Portanto, stacksampler.pyé muito fácil registrar um manipulador de sinal :
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
Ele apenas retira uma pilha de um quadro e aumenta o número de vezes que uma determinada pilha foi visualizada. Tão simples! Tão legal!
Vamos dar uma olhada em todos os nossos outros criadores de perfil que eles usam
setitimere descobrir onde no código eles chamam setitimer:
stackprof(Ruby):stackprof.c118perftools.rb(Ruby): , , , , gem (?)stacksampler(Python):stacksampler.py51statprof(Python):statprof.py239vmprof(Python):vmprof_unix.c294
O importante
setitimer- você precisa decidir como contar o tempo. Você quer 20ms em tempo real? Tempo de CPU do usuário de 20 ms? Usuário de 20 ms + tempo de CPU do sistema? Se você olhar atentamente para os links acima, notará que esses criadores de perfil usam coisas diferentes setitimer- às vezes, o comportamento é personalizável e às vezes não. A página do manual é setitimercurta e vale a pena ser lida para todas as configurações possíveis. apontou um caso de uso interessante
@mgedminno Twittersetitimer . Este problema e este problema revelam mais detalhes.
Uma desvantagem INTERESSANTE dos criadores de perfil com base em
setitimer- quais temporizadores acionam sinais! Os sinais às vezes interrompem as chamadas do sistema! Às vezes, as chamadas do sistema demoram alguns milissegundos! Se você tirar instantâneos com muita frequência, poderá fazer o programa executar chamadas de sistema indefinidamente!
Perfiladores de amostragem que não usam setitimer
Existem vários perfis de amostragem que não usam
setitimer:
pyinstrumentusaPyEval_SetProfile(é uma espécie de gerador de perfil de rastreamento), mas nem sempre coleta instantâneos de pilha quando o retorno de chamada de rastreamento é chamado. Aqui está o código que escolhe o tempo do instantâneo de rastreamento de pilha . Leia mais sobre esta solução neste blog . (basicamente:setitimerpermite que você crie o perfil apenas do thread principal em Python)pyflameperfis de código Python fora de um processo usando uma chamada de sistemaptrace. Ele usa um loop onde tira fotos, dorme por um certo tempo e faz a mesma coisa novamente. Aqui está uma chamada para esperar.python-flamegraphadota uma abordagem semelhante, onde inicia um novo thread em seu processo Python e obtém os rastreamentos de pilha, adormece e faz loops novamente. Aqui está uma chamada para esperar .
Todos os três criadores de perfil tiram instantâneos em tempo real.
Postagens do blog Pyflame
Passei quase todo o meu tempo neste post em criadores de perfil além de
pyflame, mas na verdade me interessa mais porque faz o perfil de seu programa Python de um processo separado, e é por isso que quero que meu criador de perfil Ruby funcione de forma semelhante.
Claro, tudo é um pouco mais complicado do que descrevi. Não vou entrar em detalhes, mas Evan Klitzke escreveu muitos artigos bons sobre isso em seu blog:
- Pyflame: Ptracing Profiler para Python da Uber Engineering - Introdução ao pyflame
- Modo Pyflame Dual Interpreter - como é compatível com Python 2 e Python 3 ao mesmo tempo
- Uma mudança inesperada no Python ABI - sobre como adicionar suporte para Python 3.6;
- Dumping Python Multi-Threaded Stacks ;
- Pacotes Pyflame ;
ptracee chamadas de sistema em Python;- Usando ptrace para diversão e lucro, ptrace (continuação) ;
Mais informações podem ser encontradas em eklitzke.org . Todas essas são coisas muito interessantes sobre as quais irei ler mais de perto - talvez
ptraceseja melhor do que process_vm_readvimplementar um criador de perfil Ruby! Ele tem process_vm_readvmenos sobrecarga porque não para o processo, mas também pode fornecer um instantâneo incorreto, pois não para o processo :). Em meus experimentos, obter imagens conflitantes não foi um grande problema, mas acho que aqui irei conduzir uma série de experimentos.
Isso é tudo por hoje!
Existem muitas sutilezas importantes que não falei neste post - por exemplo, eu basicamente disse isso
vmprofe stacksampler- são semelhantes (não são -vmprofsuporta criação de perfil de string e criação de perfil de funções Python escritas em C, o que, acredito, torna o criador de perfil mais complexo). Mas eles têm alguns dos mesmos princípios básicos, então acho que a revisão de hoje será um bom ponto de partida.
TDD com e sem pytest. Webinar grátis