Profiling
LidoNesta aula, você aprenderá a identificar gargalos de desempenho em seus programas Python usando as ferramentas cProfile e timeit. Vamos explorar como interpretar os relatórios de profiling e usar essas informações para guiar otimizações de forma eficiente e segura.
Bem-vindo à aula sobre profiling em Python. Profiling é o processo de medir o desempenho do seu código para descobrir onde ele gasta mais tempo ou utiliza mais recursos. Em vez de adivinhar, o profiling fornece dados concretos que orientam suas decisões de otimização. Nesta aula, vamos explorar as ferramentas padrão do Python: cProfile para análise detalhada de chamadas de função e timeit para medições precisas de trechos de código.
O objetivo não é apenas rodar o profiler, mas também entender como interpretar os resultados e transformá-los em ações eficazes. Vamos ver um exemplo prático de otimização guiada por profiling, mostrando como pequenas mudanças podem trazer grandes ganhos quando baseadas em evidências.
cProfile
cProfile é um profiler determinístico incluído na biblioteca padrão do Python. Ele rastreia cada chamada de função e registra o tempo gasto em cada uma, fornecendo um relatório detalhado. É ideal para identificar funções que consomem mais tempo, chamadas excessivas ou gargalos em aplicações reais.
Existem duas formas principais de usar cProfile: pela linha de comando ou diretamente no código. Pela linha de comando, você executa:
python -m cProfile meu_script.pyIsso executa o script e imprime um relatório no final. Para um controle mais fino, você pode usar a API em Python:
import cProfile
import pstats
def minha_funcao():
total = 0
for i in range(1000000):
total += i
return total
# Profiling da função
profiler = cProfile.Profile()
profiler.enable()
minha_funcao()
profiler.disable()
# Exibir resultados
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats()O relatório mostra colunas como `ncalls` (número de chamadas), `tottime` (tempo total na função, excluindo subchamadas), `percall` (tempo por chamada), `cumtime` (tempo acumulado, incluindo subchamadas) e `filename:lineno`. Para scripts longos, você pode salvar os resultados em um arquivo e usar ferramentas como `snakeviz` para visualização interativa.
timeit
timeit é uma ferramenta para medir o tempo de execução de pequenos trechos de código com alta precisão. Ele executa o código várias vezes e calcula a média, minimizando interferências do sistema. É perfeito para comparar diferentes implementações de uma mesma tarefa.
Você pode usar timeit pela linha de comando ou no código. Exemplo simples:
import timeit
# Medir tempo de uma expressão
print(timeit.timeit('x = sum(range(100))', number=10000))
# Medir tempo de uma função
setup = 'from math import sqrt'
code = 'result = sqrt(12345)'
print(timeit.timeit(code, setup=setup, number=100000))
# Usando Timer para repetições
from timeit import Timer
t = Timer('a = [i for i in range(1000)]')
print(t.timeit(number=1000))O parâmetro `number` define quantas vezes o código será executado. Para obter resultados mais estáveis, você pode usar `repeat` que executa a medição várias vezes e retorna uma lista de tempos. Exemplo:
import timeit
resultados = timeit.repeat('x = [i**2 for i in range(100)]', repeat=5, number=10000)
print(resultados)
print('Média:', sum(resultados)/len(resultados))timeit é especialmente útil para micro-otimizações e para tomar decisões baseadas em dados, como escolher entre uma list comprehension e um loop for.
Interpretando
Interpretar os resultados do profiling é uma habilidade crucial. No relatório do cProfile, foque nas funções com maior `cumtime` ou `tottime`. `cumtime` indica o tempo total gasto na função e em todas as suas subchamadas, ajudando a identificar gargalos em cascata. `tottime` mostra o tempo gasto apenas dentro da função, sem contar chamadas internas, útil para ver onde a função é ineficiente por si só.
Além das colunas, observe o número de chamadas (`ncalls`). Se uma função é chamada muitas vezes, mesmo pequenas otimizações podem trazer grandes ganhos. Por exemplo, se uma função de validação é chamada 1 milhão de vezes e leva 0.001s, otimizá-la para 0.0005s economiza 0.5s no total.
Considere também o contexto: o profiling pode ser enviesado por efeitos de cache, concorrência ou I/O. Repita as medições e teste em diferentes cenários. Para aplicações grandes, use amostragem (como `py-spy`) se o overhead do cProfile for alto. Com tempo, você desenvolve intuição para encontrar padrões comuns de ineficiência, como loops aninhados desnecessários, uso excessivo de atributos, ou chamadas de funções dentro de loops que poderiam ser movidas para fora.
Otimização guiada
Otimização guiada por profiling é a prática de usar os dados do profiler para decidir onde aplicar esforço. O princípio é: não otimize cegamente; meça primeiro, otimize depois. Comece identificando os maiores gargalos, faça uma alteração, meça novamente e compare.
Vamos a um exemplo prático. Suponha que temos uma função que processa uma lista de números e calcula a soma dos quadrados:
import timeit
def soma_quadrados(n):
total = 0
for i in range(n):
total += i * i
return total
# Medindo o tempo
print(timeit.timeit('soma_quadrados(1000000)', globals=globals(), number=10))O profiling com cProfile pode mostrar que a função passa a maior parte do tempo no loop. Uma otimização óbvia é usar uma expressão geradora ou a função `sum`:
def soma_quadrados_otimizada(n):
return sum(i * i for i in range(n))
# Comparação
print(timeit.timeit('soma_quadrados_otimizada(1000000)', globals=globals(), number=10))No entanto, para números grandes, a versão com gerador pode ser mais lenta devido ao overhead. Uma abordagem mais eficiente é usar a fórmula matemática da soma dos quadrados: n*(n+1)*(2n+1)/6. Isso reduz a complexidade de O(n) para O(1).
def soma_quadrados_formula(n):
return n * (n + 1) * (2 * n + 1) // 6
# Comparação final
print(timeit.timeit('soma_quadrados_formula(1000000)', globals=globals(), number=10))Esse exemplo ilustra como o profiling pode revelar que a função é chamada poucas vezes, mas o algoritmo é ineficiente. A otimização deve focar na complexidade algorítmica, não apenas em micro-ajustes.
Boas práticas
Ao trabalhar com profiling, siga estas boas práticas: sempre meça antes de otimizar; use perfis em ambientes semelhantes aos de produção; não descarte a legibilidade do código em prol de ganhos marginais; documente as otimizações e os motivos; e, por fim, teste após otimizar para garantir que o comportamento não mudou.
Exercícios
Usando cProfile, analise o seguinte código e identifique qual função tem o maior tempo cumulativo. Escreva o comando que você usaria para executar o profiling e explique o que você vê.
import random def gerar_dados(n): return [random.randint(1, 100) for _ in range(n)] def calcular_media(dados): return sum(dados) / len(dados) def principal(n): dados = gerar_dados(n) media = calcular_media(dados) return media principal(1000000)✓ Resposta: O comando seria:python -m cProfile meu_script.py. O relatório mostra quegerar_dadosprovavelmente tem o maior tempo cumulativo, pois gera 1 milhão de números aleatórios. A funçãocalcular_mediatambém consome tempo, mas menos. Usesort_stats('cumulative')para ver as funções em ordem decrescente de tempo acumulado.Use timeit para comparar duas formas de criar uma lista de quadrados: usando um loop for e usando uma list comprehension. Execute cada uma 10.000 vezes e exiba os tempos. Qual é mais rápida?
✓ Resposta: Código:
Normalmente a list comprehension é mais rápida porque é otimizada em C e evita o overhead de append.import timeit loop = """ quadrados = [] for i in range(100): quadrados.append(i*i) """ comp = "quadrados = [i*i for i in range(100)]" t1 = timeit.timeit(loop, number=10000) t2 = timeit.timeit(comp, number=10000) print('Loop:', t1) print('Comprehension:', t2) print('Comprehension é mais rápida:', t2 < t1)Dado o seguinte código, execute o cProfile e identifique o gargalo. Em seguida, sugira uma otimização e meça o ganho com timeit.
def processar(lista): resultado = [] for item in lista: if item % 2 == 0: resultado.append(item * 2) return resultado processar(range(100000))✓ Resposta: O gargalo é o loop Python. Otimização: usar list comprehension:resultado = [item * 2 for item in lista if item % 2 == 0]. Meça com timeit:
A versão com comprehension tende a ser mais rápida.import timeit original = """ def processar(lista): resultado = [] for item in lista: if item % 2 == 0: resultado.append(item * 2) return resultado processar(range(100000)) """ otimizado = """ def processar_otimizado(lista): return [item * 2 for item in lista if item % 2 == 0] processar_otimizado(range(100000)) """ print(timeit.timeit(original, number=100)) print(timeit.timeit(otimizado, number=100))Escreva um script que use cProfile para perfilhar uma função de Fibonacci recursiva (com n=30) e uma função iterativa (com n=30). Compare o número de chamadas e o tempo. Qual é mais eficiente?
✓ Resposta: Script:
A recursiva terá um número enorme de chamadas (exponencial) e será muito mais lenta. A iterativa é O(n) e muito mais eficiente.import cProfile def fib_rec(n): if n <= 1: return n return fib_rec(n-1) + fib_rec(n-2) def fib_iter(n): a, b = 0, 1 for _ in range(n): a, b = b, a + b return a cProfile.run('fib_rec(30)') cProfile.run('fib_iter(30)')Você precisa otimizar uma função que processa strings. Usando timeit, compare o uso de
+=em um loop para concatenar strings versus usar''.join(lista). Explique por que um é melhor que o outro.✓ Resposta: Código:
Oimport timeit concat = """ s = "" for i in range(1000): s += str(i) """ join = """ lista = [str(i) for i in range(1000)] s = ''.join(lista) """ print(timeit.timeit(concat, number=1000)) print(timeit.timeit(join, number=1000))joiné muito mais rápido porque strings são imutáveis;+=cria uma nova string a cada iteração, resultando em O(n^2).joiné O(n) e mais eficiente.