O Python provê algumas ferramentas para avaliar performance e localizar gargalos na aplicação. Entre estas ferramentas estão os módulos cProfile e timeit.
O módulo cProfile faz uma análise detalhada de performance, incluindo as chamadas de função, retornos de função e exceções.
Exemplo:
import cProfile
def rgb1():
"""
Função usando range()
"""
rgbs = []
for r in range(256):
for g in range(256):
for b in range(256):
rgbs.append('#%02x%02x%02x' % (r, g, b))
return rgbs
def rgb2():
"""
Função usando xrange()
"""
rgbs = []
for r in xrange(256):
for g in xrange(256):
for b in xrange(256):
rgbs.append('#%02x%02x%02x' % (r, g, b))
return rgbs
def rgb3():
"""
Gerador usando xrange()
"""
for r in xrange(256):
for g in xrange(256):
for b in xrange(256):
yield '#%02x%02x%02x' % (r, g, b)
def rgb4():
"""
Função usando uma lista várias vezes
"""
rgbs = []
ints = range(256)
for r in ints:
for g in ints:
for b in ints:
rgbs.append('#%02x%02x%02x' % (r, g, b))
return rgbs
def rgb5():
"""
Gerador usando apenas uma lista
"""
for i in range(256 ** 3):
yield '#%06x' % i
def rgb6():
"""
Gerador usando xrange() uma vez
"""
for i in xrange(256 ** 3):
yield '#%06x' % i
# Benchmarks
print 'rgb1:'
cProfile.run('rgb1()')
print 'rgb2:'
cProfile.run('rgb2()')
print 'rgb3:'
cProfile.run('list(rgb3())')
print 'rgb4:'
cProfile.run('rgb4()')
print 'rgb5:'
cProfile.run('list(rgb5())')
print 'rgb6:'
cProfile.run('list(rgb6())')
rgb1: 16843012 function calls in 24.060 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 23.070 23.070 23.875 23.875 <ipython-input-1-6dc3eae20781>:3(rgb1) 1 0.185 0.185 24.060 24.060 <string>:1(<module>) 16777216 0.739 0.000 0.739 0.000 {method 'append' of 'list' objects} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 65793 0.067 0.000 0.067 0.000 {range} rgb2: 16777219 function calls in 23.214 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 22.293 22.293 23.027 23.027 <ipython-input-1-6dc3eae20781>:14(rgb2) 1 0.187 0.187 23.214 23.214 <string>:1(<module>) 16777216 0.734 0.000 0.734 0.000 {method 'append' of 'list' objects} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} rgb3: 16777219 function calls in 23.711 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 16777217 22.172 0.000 22.172 0.000 <ipython-input-1-6dc3eae20781>:25(rgb3) 1 1.540 1.540 23.711 23.711 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} rgb4: 16777220 function calls in 23.812 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 22.840 22.840 23.609 23.609 <ipython-input-1-6dc3eae20781>:34(rgb4) 1 0.203 0.203 23.812 23.812 <string>:1(<module>) 16777216 0.770 0.000 0.770 0.000 {method 'append' of 'list' objects} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 1 0.000 0.000 0.000 0.000 {range} rgb5: 16777220 function calls in 10.513 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 16777217 8.896 0.000 9.111 0.000 <ipython-input-1-6dc3eae20781>:46(rgb5) 1 1.402 1.402 10.513 10.513 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 1 0.215 0.215 0.215 0.215 {range} rgb6: 16777219 function calls in 10.369 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 16777217 8.888 0.000 8.888 0.000 <ipython-input-1-6dc3eae20781>:53(rgb6) 1 1.481 1.481 10.369 10.369 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
O relatório do cProfile mostra no inicio as duas informações mais importantes: o tempo de CPU consumido em segundos e a quantidade de chamadas de função. As outras linhas mostram os detalhes por função, incluindo o tempo total e por chamada.
As cinco rotinas do exemplo têm a mesma funcionalidade: geram uma escala de cores RGB. Porém, o tempo de execução é diferente.
Comparando os resultados:
Rotina | Tipo | Tempo | Laços | x/range() |
---|---|---|---|---|
rgb1() |
Função | 24.060 | 3 | range() |
rgb2() |
Função | 23.214 | 3 | xrange() |
rgb3() |
Gerador | 23.711 | 3 | xrange() |
rgb4() |
Função | 23.812 | 3 | range() |
rgb5() |
Gerador | 10.513 | 1 | range() |
rgb6() |
Gerador | 10.369 | 1 | xrange() |
Fatores observados que pesaram no desempenho:
xrange()
apresentou uma performance ligeiramente melhor do que a função range()
.O gerador rgb6()
, que usa apenas um laço e xrange()
, é bem mais eficiente que as outras rotinas.
Outro exemplo:
import cProfile
def fib1(n):
"""
Fibonacci calculado de forma recursiva.
"""
if n > 1:
return fib1(n - 1) + fib1(n - 2)
else:
return 1
def fib2(n):
"""
Fibonacci calculado por um loop.
"""
if n > 1:
# O dicionário guarda os resultados
fibs = {0:1, 1:1}
for i in xrange(2, n + 1):
fibs[i] = fibs[i - 1] + fibs[i - 2]
return fibs[n]
else:
return 1
print 'fib1'
cProfile.run('[fib1(x) for x in xrange(1, 31)]')
print 'fib2'
cProfile.run('[fib2(x) for x in xrange(1, 31)]')
fib1 7049124 function calls (32 primitive calls) in 1.449 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 7049122/30 1.448 0.000 1.448 0.048 <ipython-input-2-f9357e8e26d1>:3(fib1) 1 0.000 0.000 1.449 1.449 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} fib2 32 function calls in 0.000 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 30 0.000 0.000 0.000 0.000 <ipython-input-2-f9357e8e26d1>:12(fib2) 1 0.000 0.000 0.000 0.000 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
A performance do cálculo da série de Fibonacci usando um laço que preenche um dicionário é muito mais eficiente do que a versão usando recursão, que faz muitas chamadas de função.
O módulo timeit serve para fazer benchmark de pequenos trechos de código. O módulo foi projetado para evitar as falhas mais comuns que afetam programas usados para fazer benchmarks.
Exemplo:
import timeit
# Lista dos quadrados de 1 a 1000
cod = '''s = []
for i in xrange(1, 1001):
s.append(i ** 2)
'''
print timeit.Timer(cod).timeit()
# Com Generator Expression
cod = 'list(x ** 2 for x in xrange(1, 1001))'
print timeit.Timer(cod).timeit()
# Com List Comprehesion
cod = '[x ** 2 for x in xrange(1, 1001)]'
print timeit.Timer(cod).timeit()
111.733070135 80.269646883 71.9685299397
O List Comprehension é mais eficiente do que o laço tradicional.
Outra forma de melhorar a performance de uma aplicação é usando o Psyco, que é uma espécie de Just In Time Compiler (JIT). Durante a execução, ele tenta otimizar o código da aplicação e, por isso, o módulo deve ser importado antes do código a ser otimizado (o inicio do módulo principal da aplicação é um lugar adequado).
Exemplo (com o último trecho de código avaliado no exemplo anterior):
import psyco
# Tente otimizar tudo
psyco.full()
import timeit
# Lista dos quadrados de 1 a 1000
cod = '[x ** 2 for x in xrange(1, 1001)]'
print timeit.Timer(cod).timeit()
26.678481102
O código foi executado mais de duas vezes mais rápido do que antes. Para isso, foi necessário apenas acrescentar duas linhas de código.
Porém, o Psyco deve ser usado com alguns cuidados, pois em alguns casos ele pode não conseguir otimizar ou até piorar a performance. As funções map()
e filter()
devem ser evitadas e módulos escritos em C, como o re (expressões regulares) devem ser marcados com a função cannotcompile()
para que o Psyco os ignore. O módulo fornece formas de otimizar apenas determinadas partes do código da aplicação, tal como a função profile()
, que só otimiza as partes mais pesadas do aplicativo, e uma função log()
que analisa a aplicação, para contornar estas situações.
Algumas dicas sobre otimização: