Módulo timeit

Como já escrevi em um post anterior, podemos utilizar o módulo timeit (Time it!) para medir o tempo de execução de nossos programas em Python. Porém, às vezes queremos apenas verificar qual trecho de código obtém menor tempo de execução, para escolher qual abordagem seguir. Por exemplo, quero saber o que acarreta em um menor tempo de execução: fazer um for com a função range() ou com a função xrange()?

Nada melhor do que testar na prática para descobrir qual nos dá o menor tempo de resposta. Para isso, podemos usar a interface de linha de comando que o módulo timeit fornece, quando executado como módulo.

python -m timeit

Assim, podemos testar:

python -m timeit "for i in range(1, 10000): pass"
1000 loops, best of 3: 303 usec per loop
python -m timeit "for i in xrange(1, 10000): pass"
1000 loops, best of 3: 206 usec per loop

Como podemos ver, o uso da função xrange() nos deu um tempo de execução de 97 milisegundos a menos do que utilizando a função range(), considerando o melhor dos casos para ambos. No exemplo acima, o timeit executou 3 baterias de testes compostas por 1000 execuções do código passado como argumento.

Em muitos casos iremos precisar testar um código que possui mais de uma linha. Isso pode ser feito passando cada uma das linhas como um argumento separado para o programa. Por exemplo:

x = 0
for i in range(0, 1000):
    x = i * 2

O código acima poderia ser testado pela linha de comando da seguinte forma:

python -m timeit "x=0" "for i in range(1, 10000):" "    x = i * 2"

Ou seja, cada linha é uma string separada e a indentação é feita linha por linha.

Por que python -m?

Se você está se perguntando o que significa python -m timeit, eis a resposta: ao chamar o interpretador python com a opção -m seguida pelo nome de um módulo existente no ambiente, o interpretador irá buscar o arquivo .py que representa tal módulo e executá-lo como se fosse um programa. O módulo timeit, em meu sistema, fica localizado em /usr/lib/python2.7/timeit.py (como eu sei isso? leia aqui). O que é feito quando executamos python -m timeit é o mesmo que:

python /usr/lib/python2.7/timeit.py "for i in xrange(1, 10000): pass"

Vá em frente e teste.

 

O timeit pode ser usado também dentro de programs Python. Veja mais exemplos: http://docs.python.org/library/timeit.html#examples

range() vs xrange()

(válido somente para Python 2.x)

A função range()

Em Python, é muito comum usarmos a seguinte estrutura para realizar uma repetição baseada em um contador:

for i in range(0, 10):
    print i,

A função range(x, y) gera uma lista de números inteiros de x até y (sem incluir o segundo). Assim, range(0, 10), gera a seguinte lista:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Desse modo, a variável i é iterada sobre essa lista, através da estrutura de repetição for. O trecho de código:

for i in range(0, 10):
    print i,

pode ser lido como:

"Para cada elemento na lista [0,1,2,3,4,5,6,7,8,9], imprima tal elemento"

Usar range() ou xrange()? Eis a questão…

É comum ouvirmos ou lermos algum desenvolvedor Python aconselhando a utilização da função xrange() ao invés da função range(), por questões de desempenho. Mas o que é essa tal de xrange()?

A xrange() nada mais é do que uma função que pode, em muitos casos (não sempre), substituir o uso da função range(), fornecendo ainda melhor desempenho. Veja o código abaixo:

for i in xrange(0, 10):
    print i,

O resultado dessa execução é o mesmo de quando utilizamos a função range(), porém, por gerar um elemento de cada vez, o código que utiliza a função xrange() apresenta um desempenho superior.

Quando executamos:

for i in range(0, 1000000000):
    pass

A função range() irá imediatamente gerar uma lista contendo um bilhão de inteiros e alocar essa lista na memória. Uma lista contendo um bilhão de inteiros é capaz de encher a memória de um computador pessoal.

Já com a função xrange(), ao executarmos:

for i in xrange(0, 1000000000):
    pass

Cada um dos inteiros (dos 1 bilhão) será gerado de uma vez, economizando memória e tempo de startup.

Vamos então testar o desempenho usando o módulo timeit().

 

A hora da verdade

junior@qwerty:~ $ python -m timeit "for i in xrange(10000000): pass"
 10 loops, best of 3: 246 msec per loop
junior@qwerty:~-$ python -m timeit "for i in range(10000000): pass"
 10 loops, best of 3: 342 msec per loop

Como podemos ver, o loop que utiliza a função xrange() foi quase 100 milisegundos mais rápido do que o loop que utiliza a função range(). Além disso, se fizermos uma análise de consumo de memória, veremos que a o código que utiliza a função range() utiliza uma quantidade de memória muito maior, pois gera a lista inteira antes de executar a iteração do for.

Então nunca mais vou usar o range(), certo?

Errado! Existe uma diferença fundamental entre as duas funções: a função xrange() não gera uma lista. Isso torna inviável, por exemplo, o slicing e a gravação de seu resultado como uma lista em uma variável. Vejamos:

>>> x = range(0, 10)
>>> print x
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> y = xrange(0, 10)
>>> print y
xrange(10)

Ou seja, apesar de nos fornecer um desempenho superior ao desempenho obtido com a range(), a função xrange() não substitui a anterior em todos os seus casos.

Medindo tempo de execução de código Python

Muitas vezes, é necessário que comparemos duas implementações diferentes de uma mesma função em nosso código para saber qual apresenta melhor performance, com relação ao tempo de resposta. Por exemplo, escrevemos duas funções que fazem a soma de todos os números inteiros entre x e y. Ambas funcionam corretamente, mas, gostaríamos de descobrir qual delas nos fornece o menor tempo de resposta. As duas implementações para a mesma função são apresentadas abaixo:

def soma1(x, y):
    return sum(range(x, y+1))

def soma2(x, y):
    soma = 0
    for i in range(x, y+1):
        soma += i
    return soma

Tanto a função soma1, quanto a função soma2 realizam a soma de todos os elementos entre x e y. Como descobrir qual delas nos fornece o menor tempo de resposta? A primeira solução que vem em mente é marcar o tempo antes de chamar a função e marcar o tempo depois de chamar a função. Assim o tempo de resposta seria a diferença entre os dois tempos marcados. Veja o exemplo:

import time

# verifica o tempo de resposta da função soma1
ini = time.time()
soma1(1, 1000000)
fim = time.time()
print "Função soma1: ", fim-ini

# verifica o tempo de resposta da função soma2
ini = time.time()
soma2(1, 1000000)
fim = time.time()
print "Função soma2: ", fim-ini

Assim, você obtém o tempo de execução de cada uma das funções. O problema dessa abordagem é a precisão do tempo medido. A função time.time() retorna o tempo atual em segundos passados desde 1/1/1970. Apesar de ser um número de ponto flutuante, a precisão desse número não é muito grande, além de a própria chamada da função time() já trazer consigo um overhead.

Uma alternativa mais precisa é a utilização do módulo timeit. Esse módulo é incluído com Python e foi projetado especificamente para fazer a medição de tempo de execução de programas/trechos de código Python. Por isso, é mais recomendado que utilizemos o timeit no lugar de uma solução “caseira”. Vejamos a seguir como utilizar o timeit.

Pela linha de comando

O timeit pode ser utilizado para medir o tempo de execução de código python diretamente na linha de comando. O exemplo abaixo ilustra isso:

$ python -m timeit "range(1,10000)"

A linha de comando acima executa o módulo timeit como um script e passa como entrada a expressão Python que está entre aspas. A execução do comando acima em meu sistema retornou o seguinte:

10000 loops, best of 3: 156 usec per loop

Ou seja, o timeit realizou 3 baterias compostas de um conjunto de 10000 repetições cada, executando o código range(1,10000) e, dessas 3 tentativas, a melhor delas obteve uma média de 156 microsegundos por execução do código. Através dele, podemos, por exemplo, verificar na prática que o uso do xrange() ao invés do range() nos dá um tempo de execução melhor. Veja e compare:

$ python -m timeit "xrange(1,10000)"
1000000 loops, best of 3: 0.357 usec per loop

Além do recurso de teste pela linha de comando, também podemos executar o timeit dentro de scripts Python.

Importando o módulo e usando dentro do código Python

Voltando ao exemplo das funções soma1 e soma2, no qual queremos descobrir qual delas nos dá melhor tempo de resposta, podemos fazer o seguinte, no mesmo arquivo onde estão definidas as funções:

import timeit

def soma1(x, y):
    return sum(range(x, y+1))

def soma2(x, y):
    soma = 0
    for i in range(x, y+1):
        soma += i
    return soma

t = timeit.Timer("soma1(1,1000)", "from __main__ import soma1")
print t.repeat()

t = timeit.Timer("soma2(1,1000)", "from __main__ import soma2")
print t.repeat()

O programa, quando executado, irá fazer o teste de ambas as funções. Vamos analisar as duas chamadas a funções do módulo timeit:

t = timeit.Timer("soma1(1,1000)", "from __main__ import soma1")
print t.repeat()

Na primeira linha, passamos duas entradas para o método Timer(). A primeira string indica o código a ser testado, ou seja, o código do qual estamos interessados em saber o tempo de resposta. A segunda string indica um código de inicialização, necessário para que o timeit consiga executar o código que queremos. Chamando a função repeat() sem parâmetros, são executadas 3 baterias de 1000000 execuções e, como retorno, recebemos uma lista contendo os tempos médios de execução das 3 baterias de testes. Poderíamos especificar, como no exemplo abaixo, a quantidade de baterias (2) e o número de execuções por bateria (1000).

t = timeit.Timer("soma1(1,1000)", "from __main__ import soma1")
print t.repeat(2, 1000)

Assim, podemos utilizar o módulo timeit para descobrir qual o tempo de resposta de nosso código, sem a necessidade de construir o nosso próprio módulo de testes de tempo de execução. Uma das vantagens do timeit é que, por exemplo, ele desabilita o Garbage Collector, para evitar que este interfira nos resultados.

Curioso em saber qual das duas funções (soma1 e soma2) apresentou melhor tempo de execução? Faça o teste. 🙂