Trilha de aprendizado · Nível 14 · Tutorial 2

Medir trechos de código com timeit

Construa medições repetíveis de pequenas operações e compare implementações equivalentes sem confundir preparação, execução e variação do ambiente.

  • Nível: Intermediário
  • Duração: 22 min
  • 9 passos
Medir trechos de código com timeit

O que você vai percorrer

  1. Definir uma comparação válida Antes de cronometrar, defina exatamente o que será comparado, confirme que as alternativas cumprem o mesmo contrato e escolha entradas representativas. 2 min
  2. Medir e calcular o tempo por execução Meça um lote com timeit.Timer e transforme a duração total em um tempo comparável por chamada. 3 min
  3. Separar preparação e trabalho medido Defina a fronteira do microbenchmark para medir exatamente o custo que responde à sua pergunta. 2 min
  4. Repetir sem mudar a quantidade de trabalho Mantenha o estado de cada execução equivalente para que um lote do timeit meça a mesma operação repetidamente. 3 min
  5. Calibrar o lote e repetir as medições Calibre a quantidade de execuções e colete vários tempos comparáveis para a mesma operação. 3 min
  6. Interpretar os resultados sem inventar certeza Compare séries normalizadas, resuma mínimo, mediana e dispersão e formule conclusões restritas ao que foi medido. 3 min
  7. Controlar o ambiente da comparação Reduza interferências externas, aplique um protocolo igual às alternativas e registre o contexto da medição. 2 min
  8. Decidir como tratar a coleta cíclica Entenda a política do timeit para a coleta cíclica e escolha uma configuração coerente para comparar alternativas. 2 min
  9. Aplicação final: comparar e justificar Execute um experimento completo no seu computador e formule uma conclusão limitada às condições que você mediu. 3 min

O que você vai aprender

  • Preparar entradas representativas e verificar a equivalência das alternativas antes de medir.
  • Usar timeit para repetir uma operação e calcular o tempo por execução.
  • Evitar medições inválidas causadas por mutações acumuladas ou iteradores esgotados.
  • Interpretar resultados repetidos e reconhecer quando a diferença observada não sustenta uma conclusão.

Antes de começar

  • Estimar tempo e memória com notação O grande
  • Passar funções como argumentos
  • Selecionar casos de teste e verificar exceções
  • Consumir iteradores com iter e next

Passo 1 de 9

Definir uma comparação válida

Antes de cronometrar, defina exatamente o que será comparado, confirme que as alternativas cumprem o mesmo contrato e escolha entradas representativas.

O que uma comparação responde

Meça uma pergunta pequena e explícita

Um microbenchmark mede um trecho delimitado de código sob condições escolhidas. Ele complementa a análise por notação O grande: duas soluções com a mesma ordem de crescimento ainda podem ter tempos concretos diferentes para uma entrada específica.

Neste tutorial, a pergunta será: para a mesma lista de números já disponível, qual implementação filtra os valores não negativos mais rapidamente? Isso não responde, por si só, qual delas torna a aplicação inteira mais rápida.

Delimite a fronteira

A preparação da lista e a apresentação do resultado ficam fora da pergunta inicial; apenas a filtragem será comparada.

Diagrama com uma lista de entrada à esquerda, duas funções de filtragem paralelas no centro e listas de saída idênticas à direita. A região central das funções está destacada como trecho comparado.

Uma medição útil começa ao separar entrada, operação observada e resultado.

Linha de base e contrato comum

Compare alternativas equivalentes

Escolha uma linha de base: uma implementação de referência, clara e correta. Depois compare uma alternativa que produza o mesmo resultado sob o mesmo contrato.

Aqui, filtrar_com_laco é a linha de base; filtrar_com_compreensao é a alternativa. Antes de qualquer medição, verifique o resultado, a ordem dos itens, a multiplicidade e se a entrada foi preservada quando isso fizer parte do contrato.

Duas implementações para o mesmo contrato

Execute este código em um arquivo Python no seu computador para confirmar a equivalência.

python
def filtrar_com_laco(numeros):
    resultado = []
    for numero in numeros:
        if numero >= 0:
            resultado.append(numero)
    return resultado


def filtrar_com_compreensao(numeros):
    return [numero for numero in numeros if numero >= 0]


numeros = [-3, 0, 2, -1, 2, 5]
original = numeros.copy()

resultado_base = filtrar_com_laco(numeros)
resultado_alternativa = filtrar_com_compreensao(numeros)

assert resultado_base == resultado_alternativa
assert numeros == original

print(resultado_base)  # [0, 2, 2, 5]

Exemplo

O que foi validado?

A igualdade das listas confirma valores, ordem e repetições: os dois 2 continuam presentes. A cópia original permite confirmar que nenhuma função modificou numeros. Se o contrato aceitasse outra ordem ou eliminasse duplicatas, o teste de equivalência precisaria refletir essa regra.

Entradas que sustentam a comparação

Não escolha uma única lista ao acaso

Use entradas determinísticas: ao executar de novo, você deve conseguir usar os mesmos dados. Varie características relevantes da pergunta, como tamanho e distribuição dos valores.

Para este filtro, inclua ao menos: lista vazia, lista sem valores selecionados, lista com todos os valores selecionados e uma lista mista. Assim você verifica a correção antes de medir cenários maiores.

Casos de equivalência

Estes casos verificam o contrato em situações distintas.

python
casos = [
    [],
    [-4, -1, -9],
    [0, 3, 8],
    [-2, 0, 5, -1, 5],
]

for numeros in casos:
    original = numeros.copy()
    esperado = filtrar_com_laco(numeros)
    obtido = filtrar_com_compreensao(numeros)

    assert obtido == esperado
    assert numeros == original

print("Alternativas equivalentes nos casos definidos.")

Dica

Defina o cenário antes do resultado

Não altere a entrada depois de ver qual alternativa parece melhor. Registre o tamanho e a distribuição escolhidos; uma conclusão honesta sempre vale apenas para os cenários que foram comparados.

Cheque o desenho do experimento

Comparação válida

Qual desenho é adequado para comparar as duas funções de filtragem?

Passo 2 de 9

Medir e calcular o tempo por execução

Meça um lote com timeit.Timer e transforme a duração total em um tempo comparável por chamada.

Um lote, não uma única chamada

O que o timeit mede

No seu computador, o módulo padrão timeit mede uma operação repetida. Crie um Timer com uma função sem argumentos: ela deve executar exatamente o trecho que você decidiu comparar.

Timer.timeit(number=...) executa essa função number vezes e devolve a duração total do lote, em segundos. O retorno não é o resultado da função e também não é, ainda, o tempo de uma execução.

Como ler uma medição

A duração retornada abrange todas as execuções solicitadas.

Diagrama mostrando uma função sem argumentos sendo chamada repetidamente dentro de um lote cronometrado; o tempo total do lote é dividido pela quantidade de chamadas para obter o tempo por execução.

Tempo por execução = tempo total do lote ÷ número de execuções.

Medindo as duas alternativas

benchmark_filtro.py

Salve e execute este script com Python no seu computador.

python
import timeit


def filtrar_com_laco(numeros):
    resultado = []
    for numero in numeros:
        if numero >= 0:
            resultado.append(numero)
    return resultado


def filtrar_com_compreensao(numeros):
    return [numero for numero in numeros if numero >= 0]


numeros = list(range(-500, 500))

# A equivalência foi definida e verificada antes da medição.
assert filtrar_com_laco(numeros) == filtrar_com_compreensao(numeros)

number = 10_000

timer_laco = timeit.Timer(lambda: filtrar_com_laco(numeros))
total_laco = timer_laco.timeit(number=number)

# A forma de chamada é equivalente: uma função sem argumentos por alternativa.
timer_compreensao = timeit.Timer(lambda: filtrar_com_compreensao(numeros))
total_compreensao = timer_compreensao.timeit(number=number)

por_chamada_laco = total_laco / number
por_chamada_compreensao = total_compreensao / number

print(f"Laço: {por_chamada_laco * 1_000_000:.2f} µs por execução")
print(f"Compreensão: {por_chamada_compreensao * 1_000_000:.2f} µs por execução")

Dica

Mesma fronteira para comparar

A chamada da função faz parte do trecho medido nos dois casos. Não meça uma alternativa chamando a função e a outra executando um trecho diferente: a sobrecarga precisa ser comparável.

Normalizar e interpretar a unidade

Do total ao tempo por execução

Divida sempre o total retornado por number:

tempo_por_execucao = tempo_total / number

A unidade inicial é segundo por execução. Para exibir o resultado:

  • milissegundos (ms): multiplique por 1_000;
  • microssegundos (µs): multiplique por 1_000_000.

Escolha uma única unidade para as alternativas da mesma comparação.

Calcule o tempo unitário

Um lote de number = 20_000 execuções levou 0,080 segundo. Complete a expressão para obter segundos por execução:

por_execucao = 0.080 / ____

Faça uma observação local

Forma abreviada

Também existe a conveniência timeit.timeit(lambda: operacao(), number=number), que retorna o mesmo tipo de total. Neste tutorial, mantenha Timer como interface principal: ela deixa explícito qual operação está sendo cronometrada e será reutilizada nas próximas etapas.

Registre sua medição

Execute o script. Informe o number usado, a unidade escolhida e os dois tempos por execução que apareceram. Seus valores não precisam coincidir com os de outras máquinas.

Escreva pelo menos 40 caracteres (0/40).

Passo 3 de 9

Separar preparação e trabalho medido

Defina a fronteira do microbenchmark para medir exatamente o custo que responde à sua pergunta.

A pergunta define a fronteira

O que o cronômetro deve responder?

Um microbenchmark mede apenas o trecho escolhido. Se a pergunta for “quanto custa filtrar dados que já estão disponíveis?”, crie os dados antes e cronometre somente a chamada da função.

Deixe testes de equivalência, assert e print fora do trecho: eles verificam ou apresentam o resultado, mas não fazem parte da operação comparada.

Três regiões do experimento

A fronteira do cronômetro envolve apenas a operação escolhida.

Diagrama horizontal com uma lista preparada à esquerda, uma chamada de função de filtragem no centro cercada por um cronômetro, e um resultado validado e apresentado à direita.

Preparação e validação ficam fora quando a pergunta mede somente a operação sobre uma entrada já disponível.

Dica

Inclua custos por decisão, não por acidente

Se o cenário real exige construir ou transformar a entrada a cada chamada, essa construção pertence ao trecho medido. Registre essa escolha: dois benchmarks só respondem à mesma pergunta quando têm a mesma fronteira.

Usar `stmt`, `setup` e `globals`

Preparação textual do `timeit`

Com Timer, você pode fornecer stmt e setup como textos. O setup é executado uma vez no início de cada medição de lote, antes do intervalo cronometrado — não uma vez para cada execução interna definida por number.

O dicionário globals torna funções e outros nomes do script disponíveis explicitamente para o código textual.

Medir a filtragem com dados já disponíveis

Salve como um script Python e execute no seu computador.

python
from timeit import Timer


def filtrar_laco(valores):
    resultado = []
    for valor in valores:
        if valor >= 0:
            resultado.append(valor)
    return resultado


def filtrar_compreensao(valores):
    return [valor for valor in valores if valor >= 0]

# Validação: fora da medição.
caso_teste = [-2, 0, 4, -1, 4]
assert filtrar_laco(caso_teste) == filtrar_compreensao(caso_teste)

# setup cria a entrada uma vez para este lote.
timer = Timer(
    stmt="filtrar_laco(valores)",
    setup="valores = list(range(-500, 500))",
    globals={"filtrar_laco": filtrar_laco},
)

number = 10_000
total = timer.timeit(number=number)
print(f"{total / number * 1_000_000:.2f} µs por execução")

Exemplo

Quando a construção deve entrar

Se a pergunta for “quanto custa receber valores em texto, convertê-los e filtrá-los?”, a conversão deve estar em stmt, pois ocorre em cada execução:

stmt="filtrar_laco([int(x) for x in texto.split(',')])"

Nesse caso, disponibilize texto em globals ou crie-o no setup. Não compare esse total diretamente com uma medição que recebe uma lista já pronta: são perguntas diferentes.

Classifique cada parte do experimento

Cenário: medir somente a filtragem

Associe cada ação à sua função e frequência nesse cenário: “Quanto custa filtrar uma lista que já está disponível?”

Toque em um item e depois no par correspondente.

Passo 4 de 9

Repetir sem mudar a quantidade de trabalho

Mantenha o estado de cada execução equivalente para que um lote do timeit meça a mesma operação repetidamente.

O lote precisa repetir o mesmo trabalho

Estado também faz parte da entrada

Em um microbenchmark, não basta chamar a mesma expressão várias vezes: cada chamada precisa receber um estado comparável. Se a operação modifica uma lista ou consome um iterador, as execuções seguintes podem fazer menos trabalho — ou trabalho diferente — da primeira.

Isso produz tempos que parecem bons, mas não respondem à pergunta original.

Duas formas de alterar o trabalho

Listas mutáveis podem ser transformadas; iteradores avançam até se esgotar.

Diagrama com duas sequências de execuções: uma lista desordenada que se torna ordenada após a primeira execução e um iterador com itens que diminui até ficar vazio.

A entrada deve representar o mesmo cenário em cada execução interna do lote.

Atenção

Setup não reinicia cada chamada

O setup do timeit é executado uma vez antes do lote cronometrado, não antes de cada uma das execuções internas. Portanto, criar uma lista no setup não resolve uma operação que a modifica repetidamente dentro do mesmo lote.

Renove a lista dentro da operação medida

Ordenar a mesma lista é uma comparação instável

No exemplo inválido abaixo, a primeira chamada altera dados. As chamadas posteriores recebem uma lista que já está ordenada. Como ordenar uma lista já ordenada pode ter comportamento diferente, o lote não mede repetições equivalentes.

Evite reutilizar a lista modificada

A lista é criada uma única vez e depois alterada pelo próprio trecho medido.

python
from timeit import Timer

dados = [7, 2, 9, 1, 5]

# Inválido para representar a ordenação da mesma entrada original.
tempo_total = Timer("dados.sort()", globals={"dados": dados}).timeit(number=10_000)
print(tempo_total)

Meça uma entrada renovada

Cada chamada começa com uma cópia da mesma lista-base. A cópia faz parte do tempo, pois é necessária para cumprir esse cenário.

python
from timeit import Timer

base = [7, 2, 9, 1, 5]

def ordenar_copia():
    dados = base.copy()
    dados.sort()

numero = 10_000
tempo_total = Timer(ordenar_copia).timeit(number=numero)
print(f"{tempo_total / numero:.9f} s por execução")

Dica

Declare a pergunta medida

Este código mede “copiar a entrada e ordená-la”, não apenas list.sort(). Se o cenário real já entrega uma lista descartável para ordenar, use uma operação que recrie esse estado de forma compatível e registre essa fronteira.

Não cronometre a criação de um iterador quando precisa processá-lo

Um iterador é consumível

Uma fonte obtida com iter(...), um gerador ou uma expressão geradora avança quando é percorrida. Depois de consumida, não contém mais os mesmos itens. Além disso, criar um resultado preguiçoso não é o mesmo que processar seus valores.

Se a pergunta é sobre o processamento, consuma o resultado e crie uma fonte nova dentro da função medida.

Fonte nova e consumo efetivo

sum força o consumo dos valores produzidos. Como range cria uma fonte nova a cada chamada, cada execução processa os mesmos 1.000 valores.

python
from timeit import Timer

def somar_quadrados():
    fonte = iter(range(1_000))
    return sum(valor * valor for valor in fonte)

numero = 10_000
tempo_total = Timer(somar_quadrados).timeit(number=numero)
print(f"{tempo_total / numero:.9f} s por execução")

Atenção

Criação não é consumo

Cronometrar apenas iter(range(1_000)) ou uma expressão geradora mede principalmente a criação do iterador. Para investigar o custo de processar os dados, inclua um consumidor, como sum, list ou um laço que realize o trabalho definido pelo cenário.

Diagnóstico de um lote inválido

Explique a correção

Considere este trecho:

fonte = iter(range(1_000))
Timer("sum(x * x for x in fonte)", globals={"fonte": fonte}).timeit(number=10_000)

Por que ele não mede 10.000 processamentos equivalentes? Descreva uma correção e diga se o custo de recriar a fonte entra ou não na medição.

Escreva pelo menos 120 caracteres (0/120).

Passo 5 de 9

Calibrar o lote e repetir as medições

Calibre a quantidade de execuções e colete vários tempos comparáveis para a mesma operação.

Calibração escolhe o tamanho do lote

Um lote longo o bastante

Timer.autorange() tenta valores crescentes de number até que um lote dure aproximadamente 0,2 segundo ou mais. Ele retorna uma tupla: number escolhido e a duração total daquele lote em segundos.

A calibração executa de verdade a operação. Portanto, a função cronometrada continua precisando criar um estado renovado quando trabalha com dados mutáveis ou fontes consumíveis. Para dados imutáveis, como a tupla do exemplo, reutilizar a entrada é seguro.

Da calibração à coleta

Diagrama mostrando lotes de execuções aumentando de tamanho até atingir uma duração adequada, seguido por vários lotes do mesmo tamanho e seus tempos totais.

autorange() define o número de execuções; depois, repeat() coleta vários totais usando esse mesmo número.

Calibre e repita com o mesmo Timer

`number` não é `repeat`

Em timer.repeat(repeat=5, number=number), number é a quantidade de execuções dentro de cada medição. Já repeat é a quantidade de medições separadas coletadas.

As medições ocorrem no mesmo processo: repeat() não reinicia o Python e não transforma os resultados em observações estatisticamente independentes. Por enquanto, trate a lista como várias leituras da mesma configuração.

Script de calibração e coleta

python
from timeit import Timer


def filtrar_laco(valores):
    resultado = []
    for valor in valores:
        if valor >= 0:
            resultado.append(valor)
    return resultado


# Entrada determinística e imutável: cada chamada recebe os mesmos valores.
dados = tuple(range(-500, 500))
timer = Timer(lambda: filtrar_laco(dados))

number, total_calibracao = timer.autorange()
totais = timer.repeat(repeat=5, number=number)
tempos_por_execucao = [total / number for total in totais]

print(f"Calibração: {number} execuções em {total_calibracao:.6f} s")
print("Totais dos lotes:", totais)
print("Segundos por execução:", tempos_por_execucao)

Dica

Forma de conveniência

timeit.repeat(stmt, setup, repeat, number, globals) oferece a forma de conveniência. Quando você precisa calibrar com autorange() e repetir usando a mesma configuração, manter um objeto Timer torna o fluxo mais direto.

Compare tempos normalizados

A lista contém totais de lotes

Cada elemento devolvido por repeat() é o tempo total de um lote com number execuções — não o tempo de uma chamada. Normalize cada leitura: total / number.

Se duas alternativas receberam valores de number diferentes na calibração, seus totais brutos não são comparáveis. Compare os respectivos tempos por execução, na mesma unidade.

Exemplo

Exemplo de normalização

Alternativa A: number = 1_000_000, total = 0,80 s → 0,80 / 1_000_000 = 0,8 µs por execução.

Alternativa B: number = 500_000, total = 0,50 s → 0,50 / 500_000 = 1,0 µs por execução.

Embora o total de B seja menor, A teve menor tempo por execução. A conclusão ainda depende de resultados repetidos, que serão interpretados no próximo step.

Organize a coleta

Sequência de uma medição repetida

Coloque as etapas na ordem adequada para coletar tempos por execução.

  1. Chamar `autorange()` e receber `number` e o total de calibração.
  2. Dividir cada total retornado por `number`.
  3. Chamar `repeat()` com esse `number` para obter vários totais de lote.

Complete a distinção

Em timer.repeat(repeat=5, number=200_000), o parâmetro que define quantas medições separadas serão coletadas é ___.

Passo 6 de 9

Interpretar os resultados sem inventar certeza

Compare séries normalizadas, resuma mínimo, mediana e dispersão e formule conclusões restritas ao que foi medido.

Compare séries na mesma escala

Olhe para a série, não só para um número

Após normalizar cada total pelo mesmo number, compare os tempos por execução das alternativas na mesma unidade e escala — por exemplo, microssegundos por chamada.

Uma lista de medições mostra mais do que um único resumo: revela se os pontos de uma alternativa ficam consistentemente abaixo dos da outra ou se se misturam. Essa leitura só vale se as funções produziram resultados equivalentes e realizaram a mesma quantidade de trabalho.

Dois padrões de resultado

Cada ponto representa uma medição normalizada. À esquerda há uma separação consistente; à direita, as faixas se sobrepõem.

Diagrama com dois pares de séries de pontos em escala horizontal comum: um par tem agrupamentos separados e o outro tem pontos sobrepostos e espalhados.

Separação consistente é evidência mais útil do que uma diferença isolada; sobreposição e espalhamento pedem cautela.

Resuma sem perder a variação

Mínimo, mediana e amplitude

O mínimo é uma estimativa da execução menos afetada por interferências ocasionais, mas não prova uma velocidade garantida. Ele só é útil quando o trabalho medido é válido e equivalente.

A mediana descreve o centro da série: metade dos tempos fica abaixo dela e metade acima. A amplitude (máximo - mínimo) torna visível a dispersão. Leia os três juntos: mínimo e mediana resumem posições; a amplitude revela quanto as medições variaram.

Calcular resumos em microssegundos

Os valores abaixo já são tempos por execução, em microssegundos.

python
from statistics import median

laco_us = [8.1, 8.0, 8.2, 8.1, 8.0]
compreensao_us = [7.5, 7.6, 7.5, 7.7, 7.6]

def resumir(nome, tempos):
    minimo = min(tempos)
    centro = median(tempos)
    amplitude = max(tempos) - min(tempos)
    print(
        f"{nome}: mínimo={minimo:.1f} µs, "
        f"mediana={centro:.1f} µs, "
        f"amplitude={amplitude:.1f} µs"
    )

resumir("laço", laco_us)
resumir("compreensão", compreensao_us)

Exemplo

Leitura deste exemplo

A compreensão tem mínimo de 7,5 µs, mediana de 7,6 µs e amplitude de 0,2 µs. O laço tem mínimo de 8,0 µs, mediana de 8,1 µs e amplitude de 0,2 µs. Neste conjunto de entradas e condições, a compreensão foi mais rápida de forma consistente nas medições exibidas.

Quando a resposta correta é “inconclusivo”

Não transforme variação em certeza

Considere A = [7,4, 10,2, 7,6, 10,0, 7,5] µs e B = [7,8, 7,9, 8,0, 7,9, 8,0] µs. A tem mínimo menor, mas também grande dispersão; sua mediana é 7,6 µs, enquanto a de B é 7,9 µs.

A diferença dos centros é pequena diante da instabilidade de A. Uma conclusão defensável é: nestas rodadas, a evidência é insuficiente para afirmar uma vantagem consistente. Isso não demonstra que A e B sejam iguais, nem estabelece significância estatística.

Mesmo uma vantagem consistente fica limitada às entradas, à versão do código e às condições que você mediu. Não é uma promessa sobre toda a aplicação.

Dica

Escreva conclusões com escopo

Prefira: “Para estas entradas medidas, a alternativa X apresentou menor mediana e menor mínimo.” Evite: “X sempre é mais rápida.”

Decida a conclusão defensável

Interprete duas séries

Duas implementações equivalentes foram medidas nas mesmas entradas, em µs por execução:

  • A: [7,4, 10,2, 7,6, 10,0, 7,5]
  • B: [7,8, 7,9, 8,0, 7,9, 8,0]

Qual conclusão é mais defensável?

Passo 7 de 9

Controlar o ambiente da comparação

Reduza interferências externas, aplique um protocolo igual às alternativas e registre o contexto da medição.

O ambiente também participa da medida

Comparações sob condições parecidas

O tempo de um lote não depende só do código. Processos concorrentes, carga do sistema e condições de energia podem disputar recursos e alterar os valores observados.

Antes de comparar, feche tarefas pesadas que não fazem parte do experimento, evite rodar sob depurador ou perfilador e mantenha as alternativas sobre entradas equivalentes. Isso reduz interferências; não torna duas rodadas, nem duas máquinas, perfeitamente idênticas.

Fontes de interferência

A ilustração relaciona o trecho medido a fatores externos que podem deslocar seus tempos.

Diagrama com um cronômetro de benchmark no centro ligado a processos em segundo plano, indicador de carga do sistema, cabo de energia e duas alternativas recebendo entradas equivalentes.

Controle o que for possível e registre o restante: a variação não desaparece por completo.

Dica

Condição prática

Se a comparação for importante, execute as alternativas na mesma máquina, na mesma sessão e sem outras tarefas intencionalmente pesadas. Anote qualquer condição incomum que não tenha sido possível controlar.

Aquecimento e ordem

Trate as alternativas de modo simétrico

As primeiras execuções podem se comportar diferente das execuções posteriores; esse efeito é chamado aqui de aquecimento. A calibração com autorange() já executa o trecho e pode aquecê-lo, mas não garante condições idênticas.

Defina uma política: por exemplo, calibrar cada alternativa e então usar repeat(). Aplique a mesma política e as mesmas entradas às duas. Repita a comparação também invertendo a ordem: A depois B, e em outra rodada B depois A. Assim, você pode perceber um possível efeito de ordem.

Verifique a afirmação

Como autorange() executa o trecho, ele sozinho garante que duas alternativas terão exatamente as mesmas condições de medição.

Registre para interpretar e repetir

Contexto mínimo do experimento

Ao guardar seus resultados, registre: implementação e versão do Python; sistema operacional e máquina; tamanho e distribuição das entradas; fronteira medida; política de aquecimento; number e repeat; unidade dos tempos; e se a coleta cíclica seguiu a política padrão ou foi reativada.

Esse registro não prova que o resultado vale em qualquer contexto. Ele permite interpretar o escopo da conclusão e repetir o protocolo de modo próximo.

Ajuste um protocolo frágil

Você mediu a alternativa A cinco vezes e depois B cinco vezes enquanto havia tarefas pesadas abertas. Os tempos de B pareceram piores. Que ajustes faria antes de concluir que B é mais lenta? Cite também informações que registraria.

Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 9

Decidir como tratar a coleta cíclica

Entenda a política do timeit para a coleta cíclica e escolha uma configuração coerente para comparar alternativas.

O que o timeit altera durante a medição

Ciclos e coleta

Objetos podem formar um ciclo de referências: por exemplo, A referencia B e B referencia A. Quando esse grupo deixa de ser acessível pelo restante do programa, a coleta cíclica pode recuperar esses objetos.

Por padrão, o timeit desativa temporariamente essa coleta durante o trecho cronometrado. Isso reduz uma fonte de variação em microbenchmarks comuns.

Um ciclo sem referências externas

A coleta cíclica trata grupos que continuam se referenciando, mas já não são alcançáveis pelo programa.

Diagrama de dois objetos apontando um para o outro, formando um ciclo isolado e sem conexão com a raiz do programa.

O ciclo permanece internamente conectado, embora o restante do programa não tenha mais como alcançá-lo.

Atenção

Desativar não é impedir toda liberação

Desativar a coleta cíclica não significa que toda liberação de memória foi bloqueada. A decisão do timeit afeta especificamente esse mecanismo de recuperação de ciclos; outros comportamentos de gerenciamento de memória do interpretador não são automaticamente interrompidos.

Quando reativar a coleta

A política deve seguir a pergunta

Se o cenário que você quer representar inclui a atuação da coleta cíclica, reative-a no setup. O setup é executado fora do intervalo cronometrado, mas configura o mecanismo para o lote que será medido.

Aplique a mesma política às duas alternativas e registre essa escolha no relatório. Caso contrário, uma diferença observada pode refletir configurações diferentes, e não as implementações.

Habilitando a coleta no setup

Neste exemplo, a coleta cíclica fica habilitada para a operação medida.

python
from timeit import Timer


def filtrar_laco(valores):
    return [valor for valor in valores if valor >= 0]


entrada = list(range(-500, 500))

timer = Timer(
    stmt="filtrar_laco(entrada)",
    setup="import gc\ngc.enable()",
    globals={
        "filtrar_laco": filtrar_laco,
        "entrada": entrada,
    },
)

total = timer.timeit(number=10_000)
print(f"{total / 10_000:.9f} s por execução")

Dica

Habilitar não força uma coleta

gc.enable() apenas permite que a coleta cíclica volte a atuar. Isso não força uma coleta em cada execução — e ela pode nem ocorrer durante um lote curto. Continue interpretando os resultados como uma série sujeita a variação.

Verifique sua decisão

Política de coleta

Você quer medir duas alternativas em um cenário no qual a coleta cíclica deve poder participar. Qual afirmação descreve corretamente o uso de gc.enable() no setup?

Passo 9 de 9

Aplicação final: comparar e justificar

Execute um experimento completo no seu computador e formule uma conclusão limitada às condições que você mediu.

Protocolo completo em um script

O que este experimento compara

Crie um arquivo chamado comparar_filtros.py e execute-o com python comparar_filtros.py. O script compara duas formas equivalentes de filtrar números não negativos em quatro cenários: dois tamanhos e duas distribuições de valores.

A fronteira medida é somente a chamada da função de filtragem. As entradas são criadas antes; a verificação de equivalência, a calibração, os aquecimentos e a apresentação ficam fora do tempo de cada execução. Cada função recebe a mesma lista e apenas lê seus valores, portanto não há mutação acumulada entre execuções.

Fronteira da medição

Diagrama mostrando dados determinísticos entrando em duas funções de filtragem equivalentes; somente as chamadas das funções estão dentro de uma área cronometrada, enquanto criação dos dados, verificação e relatório ficam fora.

Meça a mesma operação nas duas alternativas: uma chamada de filtragem sobre dados já disponíveis.

Script autocontido

Copie o script completo. Ele usa apenas a biblioteca padrão.

python
from __future__ import annotations

import gc
import platform
import statistics
import sys
import timeit


def filtrar_com_laco(numeros: list[int]) -> list[int]:
    resultado = []
    for numero in numeros:
        if numero >= 0:
            resultado.append(numero)
    return resultado


def filtrar_com_compreensao(numeros: list[int]) -> list[int]:
    return [numero for numero in numeros if numero >= 0]


def criar_entrada(tamanho: int, distribuicao: str) -> list[int]:
    if distribuicao == "alternada":
        return [indice if indice % 2 == 0 else -indice for indice in range(tamanho)]
    if distribuicao == "nenhum_selecionado":
        return [-indice - 1 for indice in range(tamanho)]
    raise ValueError(f"Distribuição desconhecida: {distribuicao}")


def medir_por_execucao(funcao, dados: list[int], aquecimentos: int = 3, repeticoes: int = 7):
    # A chamada abaixo é a fronteira medida. Dados já existem e não são alterados.
    operacao = lambda: funcao(dados)
    temporizador = timeit.Timer(operacao)

    # Mesma política de aquecimento para cada alternativa.
    for _ in range(aquecimentos):
        temporizador.timeit(number=1)

    # autorange também executa a operação; ela não pode mudar os dados reutilizados.
    number, _ = temporizador.autorange()
    totais = temporizador.repeat(repeat=repeticoes, number=number)
    tempos_us = [total / number * 1_000_000 for total in totais]
    return number, tempos_us


def resumir(tempos_us: list[float]) -> str:
    minimo = min(tempos_us)
    mediana = statistics.median(tempos_us)
    amplitude = max(tempos_us) - minimo
    return (
        f"mín={minimo:.2f} µs | mediana={mediana:.2f} µs | "
        f"amplitude={amplitude:.2f} µs"
    )


def main() -> None:
    alternativas = [
        ("laço", filtrar_com_laco),
        ("compreensão", filtrar_com_compreensao),
    ]
    cenarios = [
        (1_000, "alternada"),
        (1_000, "nenhum_selecionado"),
        (100_000, "alternada"),
        (100_000, "nenhum_selecionado"),
    ]
    repeticoes = 7

    print("Ambiente")
    print(f"Python: {sys.implementation.name} {sys.version.split()[0]}")
    print(f"Sistema: {platform.platform()}")
    print("Coleta cíclica: timeit a desativa durante cada medição")
    print("Fronteira: chamada de filtragem; criação, validação e relatório ficam fora")
    print(f"Aquecimentos por alternativa: 3 | medições por cenário: {repeticoes}\n")

    for tamanho, distribuicao in cenarios:
        dados = criar_entrada(tamanho, distribuicao)
        referencia = filtrar_com_laco(dados)
        assert filtrar_com_compreensao(dados) == referencia
        assert dados == criar_entrada(tamanho, distribuicao)

        print(f"Cenário: n={tamanho}, distribuição={distribuicao}")
        for nome, funcao in alternativas:
            number, tempos_us = medir_por_execucao(funcao, dados, repeticoes=repeticoes)
            print(f"  {nome:12} number={number:<8} {resumir(tempos_us)}")
        print()


if __name__ == "__main__":
    main()

Execute e leia o relatório

Antes de comparar

Execute o arquivo sem depurador ou perfilador e, se possível, com pouca atividade concorrente no computador. Não compare os totais retornados por repeat: cada alternativa pode receber um number diferente após a calibração. Compare os valores já normalizados em microssegundos por execução.

O assert confirma, em cada cenário, que as saídas têm a mesma ordem e multiplicidade. A segunda verificação confirma que as funções não alteraram a entrada. Se um assert falhar, interrompa a comparação: não há base para concluir qual alternativa é mais rápida.

Dica

Como concluir com prudência

Uma diferença que se mantém entre mínimo e mediana, com amplitudes pequenas em relação à distância observada, é evidência local a favor de uma alternativa. Se as faixas se sobrepõem muito, se a ordem muda entre execuções ou se a diferença é pequena diante da variação, registre o resultado como inconclusivo. Isso não prova igualdade; apenas indica que este experimento não sustenta uma preferência.

Seu relatório local

Após executar o script, escreva um relatório curto sobre um dos cenários. Qual foi sua conclusão — vantagem local ou resultado inconclusivo — e quais evidências a sustentam?

Escreva pelo menos 180 caracteres (0/180).

Fechamento: evidência antes de otimização

Resumo

Checklist de uma comparação confiável

Use este roteiro sempre que cronometrar um trecho pequeno.

  • Defina uma operação delimitada e compare alternativas que entregam o mesmo resultado.
  • Use entradas determinísticas e representativas; valide a equivalência antes de medir.
  • Controle o estado: cada execução precisa realizar a mesma quantidade de trabalho.
  • Declare o que fica dentro da fronteira medida e aplique a mesma política de aquecimento e coleta às alternativas.
  • Calibre, repita e normalize cada total pelo respectivo number.
  • Relate mínimo, mediana e dispersão na mesma unidade; limite a conclusão ao cenário e ambiente medidos.
  • Um microbenchmark orienta uma hipótese local, não substitui a análise do desempenho da aplicação inteira. Para localizar onde uma aplicação realmente concentra tempo, o próximo tutorial usará cProfile.

Tutorial concluído

Parabéns! Você concluiu: Medir trechos de código com timeit

Você concluiu o protocolo de microbenchmark com timeit: validou resultados, controlou a fronteira e o estado, normalizou séries de tempos e formulou conclusões compatíveis com a variação observada.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso