Benchmarking e Profiling

Neste capítulo, aprenderemos como benchmarking e criação de perfil ajudam a resolver problemas de desempenho.

Suponha que tenhamos escrito um código e ele esteja dando o resultado desejado também, mas e se quisermos executar esse código um pouco mais rápido porque as necessidades mudaram. Nesse caso, precisamos descobrir quais partes do nosso código estão tornando o programa inteiro mais lento. Nesse caso, benchmarking e criação de perfil podem ser úteis.

O que é Benchmarking?

O benchmarking visa avaliar algo em comparação com um padrão. No entanto, a questão que se coloca aqui é qual seria o benchmarking e por que precisamos dele no caso de programação de software. O benchmarking do código significa quão rápido o código está sendo executado e onde está o gargalo. Um dos principais motivos para o benchmarking é que ele otimiza o código.

Como funciona o benchmarking?

Se falamos sobre o funcionamento do benchmarking, precisamos começar comparando todo o programa como um estado atual, então podemos combinar micro benchmarks e então decompor um programa em programas menores. Para encontrar os gargalos dentro do nosso programa e otimizá-lo. Em outras palavras, podemos entendê-lo como dividir o grande e difícil problema em uma série de problemas menores e um pouco mais fáceis para otimizá-los.

Módulo Python para benchmarking

Em Python, temos um módulo padrão para benchmarking que é chamado timeit. Com a ajuda dotimeit módulo, podemos medir o desempenho de um pequeno pedaço de código Python em nosso programa principal.

Exemplo

No seguinte script Python, estamos importando o timeit módulo, que mede ainda mais o tempo gasto para executar duas funções - functionA e functionB -

import timeit
import time
def functionA():
   print("Function A starts the execution:")
   print("Function A completes the execution:")
def functionB():
   print("Function B starts the execution")
   print("Function B completes the execution")
start_time = timeit.default_timer()
functionA()
print(timeit.default_timer() - start_time)
start_time = timeit.default_timer()
functionB()
print(timeit.default_timer() - start_time)

Após executar o script acima, obteremos o tempo de execução de ambas as funções conforme mostrado abaixo.

Resultado

Function A starts the execution:
Function A completes the execution:
0.0014599495514175942
Function B starts the execution
Function B completes the execution
0.0017024724827479076

Escrevendo nosso próprio cronômetro usando a função decorador

Em Python, podemos criar nosso próprio cronômetro, que funcionará exatamente como o timeitmódulo. Isso pode ser feito com a ajuda dodecoratorfunção. A seguir está um exemplo de cronômetro personalizado -

import random
import time

def timer_func(func):

   def function_timer(*args, **kwargs):
   start = time.time()
   value = func(*args, **kwargs)
   end = time.time()
   runtime = end - start
   msg = "{func} took {time} seconds to complete its execution."
      print(msg.format(func = func.__name__,time = runtime))
   return value
   return function_timer

@timer_func
def Myfunction():
   for x in range(5):
   sleep_time = random.choice(range(1,3))
   time.sleep(sleep_time)

if __name__ == '__main__':
   Myfunction()

O script python acima ajuda na importação de módulos de tempo aleatório. Criamos a função decoradora timer_func (). Isso tem a função function_timer () dentro dele. Agora, a função aninhada pegará o tempo antes de chamar a função passada. Em seguida, ele aguarda o retorno da função e agarra o horário de término. Desta forma, podemos finalmente fazer o script python imprimir o tempo de execução. O script irá gerar a saída conforme mostrado abaixo.

Resultado

Myfunction took 8.000457763671875 seconds to complete its execution.

O que é criação de perfil?

Às vezes, o programador deseja medir alguns atributos como o uso de memória, complexidade de tempo ou uso de instruções específicas sobre os programas para medir a capacidade real desse programa. Esse tipo de medição sobre o programa é chamado de criação de perfil. A criação de perfil usa análise de programa dinâmica para fazer tal medição.

Nas seções subsequentes, aprenderemos sobre os diferentes módulos Python para criação de perfil.

cProfile - o módulo embutido

cProfileé um módulo integrado do Python para criação de perfil. O módulo é uma extensão C com sobrecarga razoável que o torna adequado para criar perfis de programas de longa execução. Após executá-lo, ele registra todas as funções e tempos de execução. É muito poderoso, mas às vezes um pouco difícil de interpretar e agir. No exemplo a seguir, estamos usando cProfile no código abaixo -

Exemplo

def increment_global():

   global x
   x += 1

def taskofThread(lock):

   for _ in range(50000):
   lock.acquire()
   increment_global()
   lock.release()

def main():
   global x
   x = 0

   lock = threading.Lock()

   t1 = threading.Thread(target=taskofThread, args=(lock,))
   t2 = threading.Thread(target= taskofThread, args=(lock,))

   t1.start()
   t2.start()

   t1.join()
   t2.join()

if __name__ == "__main__":
   for i in range(5):
      main()
   print("x = {1} after Iteration {0}".format(i,x))

O código acima é salvo no thread_increment.pyArquivo. Agora, execute o código com cProfile na linha de comando da seguinte maneira -

(base) D:\ProgramData>python -m cProfile thread_increment.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
      3577 function calls (3522 primitive calls) in 1.688 seconds

   Ordered by: standard name

   ncalls tottime percall cumtime percall filename:lineno(function)

   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:103(release)
   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:143(__init__)
   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:147(__enter__)
   … … … …

A partir da saída acima, fica claro que cProfile imprime todas as funções 3577 chamadas, com o tempo gasto em cada uma e o número de vezes que foram chamadas. A seguir estão as colunas que obtivemos na saída -

  • ncalls - É o número de ligações feitas.

  • tottime - É o tempo total gasto na função dada.

  • percall - Refere-se ao quociente de tottime dividido por ncalls.

  • cumtime- É o tempo acumulado gasto nesta e em todas as subfunções. É preciso até mesmo para funções recursivas.

  • percall - É o quociente de tempo cum dividido por chamadas primitivas.

  • filename:lineno(function) - Fornece basicamente os respectivos dados de cada função.