O percurso, e onde o tempo vai
As nove fases de compilação em ordem, com o que cada uma produziu e quanto levou — e por que o comando não executa o programa.
`dataforge ir` mostra cada fase: o HIR, o MIR, as análises, a SSA, o LIR. O que não havia era a visão de cima — as fases em ordem, o que cada uma produziu e quanto tempo levou.
Que é a pergunta que aparece quando um arquivo demora a abrir no editor: onde o tempo vai?
dataforge percurso app.df # a tabela de fases
dataforge percurso app.df --sem-tipos # pula a analise estatica
dataforge percurso --desenho # o caminho, com as ausencias
dataforge percurso app.df --json # como dado app.df — 0.526 ms no total
fase ms % o que saiu
lexer 0.064 12.2 12 tokens
parser 0.034 6.4 5 nos, 2 no topo
hir 0.007 1.2 0 acucares em 0 formas
tipos 0.143 27.2 0 erro(s), 0 aviso(s)
mir 0.050 9.5 1 corpo(s), 1 bloco(s)
analises 0.040 7.6 0 morto(s), 0 talvez nao definido(s)
ssa 0.033 6.3 0 no(s) phi
otimizar 0.092 17.4 0 oportunidade(s) em 0 passe(s)
lir 0.064 12.2 4 de 4 nos viraram fechamento (100%)
execucao — nao percorrida: executar e o que o programa faz
a fase mais cara deste arquivo: tiposEle não executa o programa, e isso é deliberado#
O percurso vai do lexer ao compilador de fechamentos e para ali. A última fase é nomeada, medida em zero e marcada como não percorrida, com o motivo escrito.
adopt Arcane.Percurso as Perc
fs := Perc.fases()
assert len(fs) is 10
assert fs[0]["fase"] is "lexer"
assert fs[9]["fase"] is "execucao" // nomeada, e NAO percorrida
d := Perc.divergencias()
assert len(d) bigger_eq 5
out $"{len(fs)} fases, {len(d)} divergencias do desenho"A armadilha que inverteu a resposta#
A primeira versão deste comando apontava a fase errada — e apontava com confiança, que é o pior jeito de errar.
| Arquivo de 12 tokens | Antes | Depois |
|---|---|---|
| fase apontada como mais cara | lir, com 93,8% | tipos, com 27,2% |
| total | 7,266 ms | 0,526 ms |
trabalho real do lir | 0,05 ms | 0,064 ms |
A causa: lir importa compilador e abre um interpretador por dentro. A primeira fase que toca um módulo paga o import dele, e o cronômetro atribui esse custo a ela.
O tempo é uma medida, não um benchmark#
Cada fase é cronometrada uma vez, nesta máquina, com esta carga. Serve para comparar as fases entre si — que é a pergunta — e não para comparar máquinas nem para afirmar que uma mudança melhorou algo.
Para isso há `Arcane.Bench`, que repete, tira mediana (e não média: uma pausa do coletor no meio da amostra a carrega para sempre) e sabe dizer, por Mann-Whitney, se a diferença é real.
adopt Arcane.Percurso as Perc
adopt Arcane.IO as IO
adopt Arcane.OS as OS
pasta := $"{OS.temp_dir()}/df-percurso-{randint(100000, 999999)}"
IO.mkdir(pasta)
alvo := $"{pasta}/exemplo.df"
IO.write(alvo, "action dobrar(n):\n yield n * 2\nout dobrar(21)\n")
r := Perc.percorrer(alvo)
assert len(r["fases"]) is 10
assert r["ms"] bigger 0
// a ultima fase existe no mapa e NAO foi percorrida
ultima := r["fases"][9]
assert ultima["fase"] is "execucao"
assert ultima["percorrida"] is no
out $"a fase mais cara: {r['mais_cara']}"
IO.remove_tree(pasta)O que cada fase entrega#
| Fase | Arquivo | O que sai |
|---|---|---|
lexer | lexer.py | tokens, com INDENT/DEDENT e interpolação |
parser | parser.py | a árvore — e quantos nós ela tem |
hir | hir.py | quantos açúcares o arquivo usa, de 5 formas; e 8 construções que não são açúcar, cada uma com o motivo |
tipos | typechecker.py | erros e avisos — e esta fase atravessa os adopt |
mir | mir.py | corpos e blocos básicos, com arestas rotuladas |
analises | mir.py | bloco morto, nome talvez não definido, constante provada |
ssa | ssa.py | nós φ das junções |
otimizar | otimizar.py | oportunidades de dobra, ramo morto e inalcançável |
lir | lir.py | quantos nós desceram para fechamento, e quantos recuaram |
execucao | interpreter.py | — não percorrida |
O que a medida mostrou, e não era o esperado#
No maior exemplo do repositório — 2.233 tokens, 1.161 nós — a fase mais cara não é o verificador de tipos nem a construção do grafo:
| Fase | Fatia | O que ela achou |
|---|---|---|
otimizar | 35,3% | 8 oportunidades, em 2 dos 3 passes |
mir | 15,4% | 21 corpos, 66 blocos |
tipos | 11,7% | 0 erros, 1 aviso |
ssa | 10,9% | 10 nós φ |
lir | 3,9% | 1.113 de 1.153 nós compilados (97%) |
O passe que acha menos é o que custa mais. relatorio_de reescreve a árvore inteira para contar, e num arquivo que já está bem escrito ele encontra oito dobras de constante e nada mais.
O caminho real, e onde ele difere do desenho#
dataforge percurso --desenho imprime o caminho com as ausências no lugar delas. As cinco divergências em relação ao desenho da referência:
| No desenho | Aqui | Por quê |
|---|---|---|
Borrow + Dataflow Checker | o Dataflow existe; o Borrow, não | num mundo com coletor, o que se protege é o protocolo — e quem o protege é Arcane.Posse mais três códigos do check |
LLVM Backend | compilador.py — fechamentos | o LLVM tiraria a zero dependência. Medido: 1,5× a 1,8×, com teto ~6,5× |
Machine Code / WASM | não há; o artefato é a árvore compilada | rodar em WASM funciona pelo Pyodide, e isso é outra frase |
Bare-Metal Runtime | não há | o runtime é o CPython |
(ausente no desenho) Execution Engine | interpreter.py — o centro | o desenho supõe compilação antecipada, e por isso não tem onde pôr o interpretador |