Python para Desenvolvedores

2ª edição, revisada e ampliada

Capítulo 39: Performance


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:

In [1]:
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:

  • A complexidade do algoritmo.
  • Geradores apresentaram melhores resultados do que as funções tradicionais.
  • O gerador 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:

In [2]:
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:

In [3]:
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):

In [ ]:
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:

  • Mantenha o código simples.
  • Otimize apenas o código aonde a performance da aplicação é realmente crítica.
  • Use ferramentas para identificar os gargalos no código.
  • Evite funções recursivas.
  • Use os recursos nativos da linguagem. As listas e dicionários do Python são muito otimizados.
  • Use List Comprehensions ao invés de laços para processar listas usando expressões simples.
  • Evite funções dentro de laços. Funções podem receber e devolver listas.
  • Use geradores ao invés de funções para grandes sequências de dados.
In [1]:
 
Out[1]: