Pular para o conteúdo

    Capítulo 18, Básico

    Medir desempenho de verdade

    Otimizar sem medir é adivinhar. Este capítulo mostra como medir o tempo e a memória de um trecho de código sem se enganar, e prova a afirmação do capítulo 4 com números.

    Código deste capítulo: básico/cap18_medir_desempenho.py

    O `timeit` e o melhor de vários

    Um tempo medido uma vez é ruído: o processador pode estar ocupado, o cache pode estar frio. O módulo timeit repete a medida, e a regra é usar o menor dos tempos, que é o menos afetado pelo que o seu computador estava fazendo ao mesmo tempo:

    básico/cap18_medir_desempenho.pylinhas 10 a 16
    import timeit
    
    import numpy as np
    
    a = np.arange(1_000_000)
    tempos = timeit.repeat(lambda: a.sum(), number=20, repeat=5)
    print("melhor tempo por chamada abaixo de 10 ms:", min(tempos) / 20 < 0.01)
    
    Saída
    melhor tempo por chamada abaixo de 10 ms: True
    

    Em um notebook, %timeit a.sum() faz tudo isso por você. Em um script, use timeit.repeat e olhe o min.

    Quanto custa começar: tamanhos pequenos não contam a história

    Chamar uma função do NumPy tem um custo fixo (verificar tipos, alocar o array de saída). Para poucos elementos, esse custo domina, e o array pode até perder para uma lista. A vantagem só aparece, e cresce, quando os dados crescem:

    básico/cap18_medir_desempenho.pylinhas 21 a 30
    def razao(n):
        lista = list(range(n))
        array = np.arange(n)
        t_lista = min(timeit.repeat(lambda: [x * 2 for x in lista], number=50, repeat=5))
        t_array = min(timeit.repeat(lambda: array * 2, number=50, repeat=5))
        return t_lista / t_array
    
    
    print("a vantagem cresce com o tamanho:", razao(100_000) > razao(100))
    print("em 100 mil elementos o array ganha:", razao(100_000) > 5)
    
    Saída
    a vantagem cresce com o tamanho: True
    em 100 mil elementos o array ganha: True
    

    Por isso, medir com 10 elementos e concluir que "o NumPy não é mais rápido" é um erro clássico. O teste tem de ser feito no tamanho real dos seus dados.

    Medir memória

    Tempo é só uma metade. Muitas análises falham por falta de memória, e não por lentidão. O tracemalloc mede a memória alocada pelo Python, e o NumPy reporta os seus arrays a ele:

    básico/cap18_medir_desempenho.pylinhas 35 a 41
    import tracemalloc
    
    tracemalloc.start()
    x = np.zeros(1_000_000)
    atual, pico = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    print(round(atual / 1e6, 1), "MB")
    
    Saída
    8.0 MB
    

    Um milhão de float64 ocupa 8 MB, exatamente size * itemsize. Para um array que já existe, o nbytes dá a resposta sem medir nada.

    Armadilhas de medição

    ArmadilhaO que aconteceO que fazer
    Medir uma só vezRuído dominaRepetir e usar o menor tempo
    Medir dados minúsculosO custo fixo dominaUsar o tamanho real
    Incluir a criação dos dadosMede a coisa erradaCriar fora da função medida
    Comparar resultados diferentesA conta mais rápida pode estar erradaConferir com np.allclose antes
    Confiar em número absolutoMuda de máquina para máquinaComparar razões
    Otimizar o que não é gargaloEsforço sem efeitoPerfilar antes (cProfile)

    Meça antes de mexer

    Eu só otimizo depois de saber onde o tempo vai. Para um programa inteiro, o cProfile (capítulo 48 do curso de Python) mostra qual função consome o tempo. Para um trecho, o timeit compara alternativas. E, antes de aceitar uma versão mais rápida, eu confiro que ela produz o mesmo resultado.

    Exercício 1

    Comparar duas implementações

    Escreva comparar(f, g) que devolva quantas vezes f é mais lenta que g (a razão entre os melhores tempos, em 5 repetições). Use para comparar sum(range(10000)) com np.arange(10000).sum().