Pular para o conteúdo

    Capítulo 48, Avançado

    Desempenho e profiling

    A regra de ouro: meça antes de otimizar. Quase sempre o gargalo está em um lugar diferente do que você imagina.

    Código deste capítulo: avancado/cap48_desempenho.py

    Medir um trecho com timeit

    O timeit executa um trecho várias vezes e reduz o ruído. Ele responde "qual das duas formas é mais rápida?". Aqui, buscar um item em uma lista (varre tudo, O(n)) contra buscar em um conjunto (tabela hash, O(1) em média):

    avancado/cap48_desempenho.pylinhas 10 a 15
    import timeit
    
    setup = "dados = list(range(10_000)); conjunto = set(dados)"
    na_lista = timeit.timeit("9_999 in dados", setup=setup, number=2_000)
    no_conjunto = timeit.timeit("9_999 in conjunto", setup=setup, number=2_000)
    print("conjunto mais rápido:", no_conjunto < na_lista)
    
    Saída
    conjunto mais rápido: True
    

    Eu não mostro os tempos porque mudam de máquina para máquina. A relação entre eles é o que importa, e ela só cresce com o tamanho dos dados.

    Achar o gargalo com cProfile

    Quando o programa inteiro está lento, ninguém sabe onde. O cProfile mostra quanto tempo cada função consome. As colunas que importam: ncalls (quantas chamadas), tottime (tempo dentro da função, sem contar as chamadas internas) e cumtime (tempo acumulado, contando tudo o que ela chamou):

    avancado/cap48_desempenho.pylinhas 20 a 41
    import cProfile
    import io
    import pstats
    
    
    def lento():
        return sum(i * i for i in range(200_000))
    
    
    def principal():
        for _ in range(5):
            lento()
    
    
    perfil = cProfile.Profile()
    perfil.enable()
    principal()
    perfil.disable()
    
    relatorio = io.StringIO()
    pstats.Stats(perfil, stream=relatorio).sort_stats("cumulative").print_stats(5)
    print(relatorio.getvalue())
    

    Para perfilar um script inteiro, sem alterar o código:

    Terminal
    python -m cProfile -o perfil.out meu_script.py
    python -m pstats perfil.out
    

    O algoritmo vence a micro-otimização

    Trocar for por compreensão ganha alguns por cento. Trocar um algoritmo O(n²) por O(n) ganha ordens de grandeza. Veja a detecção de duplicatas:

    avancado/cap48_desempenho.pylinhas 46 a 63
    def tem_duplicado_lento(itens):
        for i, a in enumerate(itens):
            for b in itens[i + 1:]:
                if a == b:
                    return True
        return False
    
    
    def tem_duplicado_rapido(itens):
        return len(set(itens)) != len(itens)
    
    
    amostra = list(range(2_000))
    print(tem_duplicado_lento(amostra) == tem_duplicado_rapido(amostra))
    
    t_lento = timeit.timeit(lambda: tem_duplicado_lento(amostra), number=3)
    t_rapido = timeit.timeit(lambda: tem_duplicado_rapido(amostra), number=3)
    print("a versão com set é mais rápida:", t_rapido < t_lento)
    
    Saída
    True
    a versão com set é mais rápida: True
    

    Memória

    Para medir o consumo de memória, o tracemalloc registra o pico. O gerador mostra a vantagem de não materializar a sequência inteira:

    avancado/cap48_desempenho.pylinhas 68 a 81
    import tracemalloc
    
    
    def pico(funcao):
        tracemalloc.start()
        funcao()
        _, maximo = tracemalloc.get_traced_memory()
        tracemalloc.stop()
        return maximo
    
    
    pico_lista = pico(lambda: sum([n for n in range(200_000)]))
    pico_gerador = pico(lambda: sum(n for n in range(200_000)))
    print("gerador usa menos memória:", pico_gerador < pico_lista)
    
    Saída
    gerador usa menos memória: True
    

    O que eu verifico antes de mexer em desempenho

    SintomaSuspeitaFerramenta
    O programa todo é lentoAlgoritmo, ou espera de I/OcProfile
    Um trecho específico parece lentoAlternativas equivalentestimeit
    Consumo alto de memóriaListas completas em vez de geradorestracemalloc
    Início do programa demoraImports pesadospython -X importtime meu_script.py
    Cálculo numérico lentoLaço Python sobre númerosNumPy ou outra biblioteca em C

    Ordem das ações

    Primeiro, o algoritmo e as estruturas de dados. Depois, evitar trabalho repetido (functools.cache, mover cálculos para fora do laço). Depois, trocar o trabalho em Python puro por bibliotecas em C. Concorrência e reescrita em outra linguagem ficam por último, porque são as mudanças mais caras de manter.

    Exercício 1

    Interseção sem laço duplo

    A função intersecao_lenta(a, b) é O(n por m). Escreva intersecao_rapida com o mesmo resultado, usando um conjunto.