O desempenho não se resume apenas à CPU: criando seus próprios criadores de perfil para Python

Suponha que seu programa Python seja lento e você descubra que isso se deve apenas parcialmente à falta de recursos do processador . Como descobrir quais partes do código são forçadas a esperar algo que não se aplica à CPU?



Depois de ler o material, cuja tradução publicamos hoje, você aprenderá a escrever seus próprios criadores de perfil para código Python. Estamos falando de ferramentas que detectarão locais inativos no código enquanto aguardam a liberação de determinados recursos. Em particular, discutiremos o seguinte aqui:

  • O que o programa pode esperar?
  • Criando perfil do uso de recursos que não são recursos da CPU.
  • Criação de perfil de opções de contexto não intencionais.

O que o programa espera?


Nos momentos em que o programa não está ocupado com cálculos intensivos usando o processador, parece estar esperando por algo. Isto é o que pode causar inação no programa:

  • Recursos de rede. Isso pode incluir aguardar a conclusão das pesquisas de DNS, aguardar uma resposta de um recurso de rede, aguardar o carregamento de alguns dados e assim por diante.
  • Disco rígido A leitura dos dados do disco rígido pode levar algum tempo. O mesmo pode ser dito sobre a gravação em disco. Às vezes, operações de leitura ou gravação são executadas apenas usando um cache localizado na RAM. Com essa abordagem, tudo acontece muito rapidamente. Mas, às vezes, quando um programa interage diretamente com um disco, essas operações acabam sendo bastante lentas.
  • Fechaduras. Um programa pode esperar para desbloquear um thread ou processo.
  • Suspensão do trabalho. Às vezes, um programa pode pausar deliberadamente o trabalho, por exemplo, pausando entre tentativas de executar alguma ação.

Como encontrar os locais de programas em que algo acontece que afeta muito o desempenho?

Método número 1: análise do tempo durante o qual o programa não usa o processador


O criador de perfil incorporado do Python, cProfile , é capaz de coletar dados sobre muitos indicadores diferentes relacionados à operação de programas. Devido a isso, ele pode ser usado para criar uma ferramenta com a qual você pode analisar o tempo durante o qual o programa não utiliza os recursos do processador.

O sistema operacional pode nos dizer exatamente quanto tempo o processador usou.

Imagine que estamos criando um perfil de um programa de thread único. Os programas multithread são mais difíceis de criar perfil e descrever esse processo também não é fácil. Se o programa foi executado por 9 segundos e, ao mesmo tempo, usou o processador por 7,5 segundos, isso significa que ele passou 1,5 segundos esperando.

Primeiro, crie um cronômetro que medirá o tempo limite:

 import os def not_cpu_time():    times = os.times()    return times.elapsed - (times.system + times.user) 

Em seguida, crie um profiler que analise desta vez:

 import cProfile, pstats def profile_not_cpu_time(f, *args, **kwargs):    prof = cProfile.Profile(not_cpu_time)    prof.runcall(f, *args, **kwargs)    result = pstats.Stats(prof)    result.sort_stats("time")    result.print_stats() 

Depois disso, você pode criar um perfil de várias funções:

 >>> profile_not_cpu_time( ...   lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function)    3  0.050  0.017 _ssl._SSLSocket.read    1  0.040  0.040 _socket.getaddrinfo    1  0.020  0.020 _socket.socket.connect    1  0.010  0.010 _ssl._SSLSocket.do_handshake  342  0.010  0.000 find.str  192  0.010  0.000 append.list 

Os resultados permitem concluir que a maior parte do tempo foi gasta na leitura de dados do soquete, mas demorou algum tempo para realizar pesquisas de DNS ( getaddrinfo ), bem como para realizar handshakes TCP ( connect ) e TLS / SSL.

Como nos certificamos de investigar os períodos da operação do programa em que ele não utiliza os recursos do processador, sabemos que tudo isso é tempo de espera puro, ou seja, o momento em que o programa não está ocupado com nenhum cálculo.

Por que há tempo registrado para str.find e list.append ? Ao executar essas operações, o programa não tem nada a esperar, portanto a explicação parece plausível, segundo a qual estamos lidando com uma situação em que todo o processo não foi executado. Talvez - aguardando a conclusão de algum outro processo, ou aguardando a conclusão do carregamento de dados na memória a partir do arquivo de troca. Isso indica que foi gasto algum tempo na execução dessas operações, o que não faz parte do tempo do processador.

Além disso, quero observar que vi relatórios que contêm pequenos fragmentos negativos de tempo. Isso implica uma certa discrepância entre o tempo decorrido e o tempo do processador, mas não espero que isso tenha um impacto significativo na análise de programas mais complexos.

Método número 2: análise do número de comutadores de contexto intencionais


O problema de medir o tempo gasto pelo programa na espera de algo é que, ao executar diferentes sessões de medição para o mesmo programa, isso pode variar devido a algo que está fora do escopo do programa. Às vezes, as consultas DNS podem ser mais lentas que o normal. Às vezes, mais lentamente que o normal, alguns dados podem carregar. Portanto, seria útil usar alguns indicadores mais previsíveis que não estão vinculados à velocidade do que envolve o programa.

Uma maneira de fazer isso é calcular quantas operações que precisam aguardar concluíram o processo. Ou seja, estamos falando sobre o cálculo do número de períodos de espera, e não do tempo gasto na espera de algo.

Um processo pode parar de usar os recursos do processador por dois motivos:

  1. Cada vez que um processo executa uma operação que não termina instantaneamente, por exemplo, lê dados de um soquete, pausa e assim por diante, isso é equivalente ao que diz ao sistema operacional: "Me acorde quando eu puder continuar trabalhando". Essa é a chamada "troca de contexto deliberada": o processador pode mudar para outro processo até que os dados apareçam no soquete ou até que nosso processo saia do modo de espera, assim como em outros casos semelhantes.
  2. “Mudança de contexto não intencional” é uma situação em que o sistema operacional interrompe temporariamente um processo, permitindo que outro processo tire proveito dos recursos do processador.

Vamos traçar um perfil de opções de contexto intencionais.

Vamos escrever um criador de perfil que conte com psutil contexto intencional usando a biblioteca psutil :

 import psutil _current_process = psutil.Process() def profile_voluntary_switches(f, *args, **kwargs):    prof = cProfile.Profile(        lambda: _current_process.num_ctx_switches().voluntary)    prof.runcall(f, *args, **kwargs)    result = pstats.Stats(prof)    result.sort_stats("time")    result.print_stats() 

Agora, vamos analisar o código que funciona com a rede novamente:

 >>> profile_voluntary_switches( ...   lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function)     3  7.000  2.333 _ssl._SSLSocket.read     1  2.000  2.000 _ssl._SSLSocket.do_handshake     1  2.000  2.000 _socket.getaddrinfo     1  1.000  1.000 _ssl._SSLContext.set_default_verify_path     1  1.000  1.000 _socket.socket.connect 

Agora, em vez de dados do tempo de espera, podemos ver informações sobre o número de alternâncias intencionais de contexto que ocorreram.

Observe que, às vezes, você pode ver alternâncias de contexto intencionais em locais inesperados. Eu acredito que isso acontece quando os dados do arquivo de paginação estão sendo carregados devido a erros de paginação de memória.

Sumário


O uso da técnica de criação de perfil de código descrita aqui cria uma certa carga adicional no sistema, o que diminui bastante o programa. Na maioria dos casos, no entanto, isso não deve levar a uma distorção significativa dos resultados devido ao fato de não analisarmos o uso dos recursos do processador.

Em geral, pode-se notar que quaisquer indicadores mensuráveis ​​relacionados ao trabalho do programa se prestam à criação de perfis. Por exemplo, o seguinte:

  • O número de leituras ( psutil.Process().read_count ) e gravações ( psutil.Process().write_count ).
  • No Linux, o número total de bytes lidos e gravados (psutil. Process().read_chars ).
  • Indicadores de alocação de memória (realizar essa análise exigirá algum esforço; isso pode ser feito usando o jemalloc ).

Detalhes sobre os dois primeiros itens desta lista podem ser encontrados na documentação da psutil .

Caros leitores! Como você perfila seus aplicativos Python?

Source: https://habr.com/ru/post/pt464133/


All Articles