Medir sem inventar ganho
As três formas de um teste de concorrência medir a máquina em vez do código.
Concorrência é a área onde a medida engana mais, e onde um número errado é mais convincente. Três erros aparecem sempre, e os três já reprovaram o CI deste repositório.
1. Limite absoluto mede a máquina#
assert ms < 500 responde sobre o runner, e não sobre o algoritmo. Ele passa no seu computador e reprova num CI de três núcleos — e a reação natural (afrouxar o limite) tira o pouco que ele tinha de valor.
adopt Arcane.Bench as B
action trabalho():
total := 0
cycle i in range(0, 20000):
total += i
yield total
// Medir é fácil; o difícil é o que se COBRA da medida.
// O segundo parâmetro é o ARGUMENTO da ação, e o terceiro as
// repetições — trocar os dois é o erro mais comum aqui. Sem
// argumento, a ação é chamada sem nenhum.
r := B.medir(trabalho)
assert r["ms"] >= 0
assert r["por_segundo"] > 0
out $"{round(r['ms'], 2)} ms — e este número não serve de limite"2. A razão precisa de trabalho suficiente#
Um teste de paralelismo comparou razão — o padrão certo — e falhou no Windows com 1,47: o paralelo levou 0,44 s contra 0,30 s da série. O paralelismo estava certo; o que dominou foi o custo de criar cinco threads, que no Windows passa de 60 ms de trabalho. A correção não foi afrouxar: foi dar à medida um numerador maior.
adopt Arcane.Concurrent as C
adopt Arcane.Bench as B
action com_espera(x):
sleep(40) // E/S de mentira: o GIL é solto aqui
yield x
action em_serie():
yield [com_espera(i) cycle i in range(0, 4)]
action em_paralelo():
yield C.map(com_espera, [0, 1, 2, 3])
serie := B.medir(em_serie)
junto := B.medir(em_paralelo)
razao := serie["ms"] / max(junto["ms"], 0.001)
// A cobrança é um FATOR, e com folga: quatro esperas de 40 ms em
// série são ~160 ms, e juntas são ~40 ms.
assert razao > 1.5
out $"{round(razao, 2)}× — e o limite cobrado é um fator, não um prazo"3. O `p` sozinho reprova por desenho#
Alfa de 0,05 significa que uma em vinte comparações de coisas iguais cruza o limiar. Um teste que compara uma ação com ela mesma e só olha o p falha 5% das vezes — por definição, e não por defeito.
adopt Arcane.Perfil as P
action trabalho():
total := 0
cycle i in range(0, 3000):
total += i
yield total
// 'comparar' exige as DUAS perguntas: a ordem das amostras é
// acidente (o p), e daí? (o efeito, com piso). E ele alterna a ordem
// dentro da volta, porque quem mede primeiro paga a entrada dela.
r := P.comparar(trabalho, trabalho, {"amostras": 15})
out $"comparando uma ação com ela mesma: {r['mais_rapido']}"
out $" fator {r['fator']}, p = {r['p_valor']}, significativo: {r['significativo']}"
assert r["mais_rapido"] is "empate"Uma ferramenta que responde "3% mais rápida" a isso é pior que nenhuma ferramenta: é assim que se escolhe a implementação errada com convicção.
O ponto de calibração#
Quando nem o fator basta — porque a máquina está disputada —, a saída é medir, no mesmo instante, um algoritmo cujo comportamento não está em dúvida. Se ele não der o esperado, a máquina não está medindo, e o teste diz isso e pula. Medido aqui, com seis threads queimando CPU: o linear foi de 1,98 para 3,30–4,90.