No último post falei um pouco sobre como usar cache para melhorar o desempenho do fibonacci recursivo. Os gráficos que gerei foram feitos usando Gnuplot.
Como queria uma quantidade significativa de dados para estes gráficos, não tinha como gerá-los manualmente. Precisava automatizar.
Já havia usado a ferramenta time
, uma utilidade oferecida pelo
bash
, mas tive
dificuldade em formatar a saída, que tem a seguinte forma:
bla@notebook:~$ time python3 fibonacci.py 25
real 0m0.064s
user 0m0.056s
sys 0m0.008s
Depois de várias buscas na internet descobri que existe um programa externo ao
bash
também chamado time
que também mede o tempo de execução e aceita
operadores de formato. O programa está instalado em /usr/bin/
(pelo menos no
meu notebook com Debian) e, embora este diretório
faça parte da variável de ambiente PATH
, é ofuscado pelo time
do bash
.
Para usá-lo então tive que invocar usando o caminho para o arquivo. Por preferência
pessoal usei o caminho absoluto (/usr/bin/time
).
A saída do /usr/bin/time
é no formato:
bla@notebook:~$ /usr/bin/time python3 fibonacci.py 25
0.06user 0.00system 0:00.06elapsed 95%CPU (0avgtext+0avgdata 7724maxresident)k
0inputs+0outputs (0major+865minor)pagefaults 0swaps
Como estava interessado apenas no tempo total usado (elapsed na saída acima), pude usar uma string de formato bem simples para pegar apenas este campo.
bla@notebook:~$ /usr/bin/time -f "%E" python3 fibonacci.py 25
0:00.06
Para remover a parte dos minutos (antes do :
) e ter a saída como um número
facilmente interpretável usei o programa cut
.
No caso em questão bastou determinar o delimitador de campos como :
e o campo
de interesse como 2.
bla@notebook:~$ /usr/bin/time -f "%E" python3 fibonacci.py 25 | cut -d: -f2
0:00.07
Isso não foi exatamente como desejado. O que está acontecendo aqui é que o |
está redirecionando o stdout
do comando anterior a ele para o
stdin
do cut
, mas o /usr/bin/time
não imprime no stdout
e sim no
stderr
!
Uma forma de contornar este problema é redirecionar o stderr
para o stdout
.
Felizmente a sintaxe de script do bash
nos permite fazer isto facilmente.
A saída padrão (stdout
) é representada como 1
e a saída de err (stderr
) é
representada como 2
. Para redirecionar o stderr
para o stdout
podemos usar
2>&1
. É possível fazer o contrário também (redirecionar stdout
para stderr
)
usando 1>&2
. Então, usando a primeira forma, atingimos o resultado esperado.
bla@notebook:~$ /usr/bin/time -f "%E" python3 fibonacci.py 25 2>&1 | cut -d: -f2
00.05
Para testar o tempo de execução fibonacci.py
para vários números bastava colocar
tudo isto num laço e jogar a saída em um arquivo.
bla@notebook:~$ for n in {1..5}
> do
> /usr/bin/time -f "%E" python3 fibonacci.py $n 2>&1 | cut -d: -f2 > tempos.txt
> done
00.02
00.02
00.02
00.02
00.02
Foi assim que gerei os dados dos gráficos do post anterior. Desde então tenho me perguntado se esta é a solução mais pythônica. Acredito que não, é excessivamente complexo. Então comecei a mexer no código para que não precisasse fazer todo esse malabarismo com o shell.
Medindo tempo com python
A tarefa de medir o tempo de execução de pedaços de código é geralmente parte do processo de profiling, que visa detectar gargalos de desempenho. Como este processo é bastante usado, também são as subtarefas que o compõe. Medir o tempo de execução não é exceção.
Usando time.time()
diretamente
Como toda (ou pelo menos quase toda) linguagem de programação, python oferece um
módulo para trabalhar com medidas de tempo na biblioteca padrão. A função
time
(pertencente ao módulo de mesmo nome) retorna a quantidade de segundos passados
desde epoch (1 de janeiro de 1970).
Esta representação de tempo popular é também conhecida como Unix time.
Com esta função é possível medir o tempo de execução de trechos de código facilmente. Por exemplo:
import time
def slow_func():
# Função de interesse
time.sleep(10)
tempo_inicial = time.time()
slow_func()
tempo_final = time.time()
tempo_total = tempo_final - tempo_inicial
print('{:.3f}s'.format(tempo_total))
Este código já pode ser usado dentro de um laço para gerar todos os tempos no formato desejado de uma forma mais limpa que apresentada anteriormente mas, será que pode ficar melhor?
Fazendo um decorator
Sim! Usando um decorator podemos tornar este código mais reutilizável e pythônico!
import time
import functools
def _fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
def temporizador(func):
functools.wraps(func)
def wrapper(*args, **kwargs):
tempo_inicio = time.time()
res = func(*args, **kwargs)
tempo_fim = time.time()
tempo_total = tempo_fim - tempo_inicio
print('{} executou em: {:.3f}s'.format(func.__name__, fim - inicio))
return res
return wrapper
@temporizador
def fibonacci(n):
return _fibonacci(n)
Assumindo que o código acima está no arquivo fibonacci_com_decorator.py
podemos
usá-lo no prompt interativo da seguinte forma:
>>> from fibonacci_com_decorator import *
>>> fibonacci(3)
fibonacci executou em: 0.000s
2
>>> fibonacci(25)
fibonacci executou em: 0.053s
75025
Foi necessário adicionar um wrapper para a função fibonacci devido a sua natureza recursiva. Se o decorator fosse aplicado à função recursiva os tempos de execução de todas as chamadas recursivas seriam mostrados.
O ponto mais interessante é que este método permite medir o desempenho de diferentes partes do código executando-o apenas uma vez. Além disto, os dados de performance podem ser usados em logs, permitindo que a aplicação seja analisada sem ser interrompida.
Usando um gerenciador de contexto
Não lembrava desta abordagem até ler um artigo de um amigo meu sobre aceleração de consultas no MongoDB com tornado.
Provavelmente você já se deparou com um gerenciador de contexto,
principalmente se já viu algum código que mexe com arquivos.
Um gerenciador de contexto é qualquer objeto que implementa a interface
de gerenciamento de contexto (métodos __enter__
e __exit__
). Os
objetos retornados por open
e funções relacionadas a manipulação de
arquivos são os exemplos mais conhecidos.
Usando estes objetos em um bloco with
fazemos com que o método __enter__
seja executado ao entrar no bloco e __exit__
ao sair do bloco. No caso de
arquivos geralmente temos:
with open('some_file_path') as f:
pass # ler de f
# f não está mais aberto
Ao sair do bloco o arquivo é fechado (i.e. f.close()
) evitanto que precisemos
lembrar de fazer isto.
Como qualquer objeto que implementa a interface de gerenciador de contexto pode
ser usado com o with
podemos fazer um temporizador. Para isto basta fazer com
que no método __enter__
comece a marcar o tempo e no __exit__
a diferença.
import time
import sys
class Timer:
def __enter__(self):
self.tempo = time.time()
return self
def __exit__(self, *args):
self.tempo = time.time() - self.tempo
print('Tempo gasto: {}s'.format(self.tempo))
def slow_func():
time.sleep(10)
with Timer():
slow_func()
Executando o código acima:
bla@notebook:~$ python context_timer.py
Tempo gasto: 10.01007342338562s