Pular para o conteúdo

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?

bash
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
dataforge percurso — a saída
  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: tipos

Ele 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.

dataforge
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 tokensAntesDepois
fase apontada como mais caralir, com 93,8%tipos, com 27,2%
total7,266 ms0,526 ms
trabalho real do lir0,05 ms0,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.

dataforge
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#

FaseArquivoO que sai
lexerlexer.pytokens, com INDENT/DEDENT e interpolação
parserparser.pya árvore — e quantos nós ela tem
hirhir.pyquantos açúcares o arquivo usa, de 5 formas; e 8 construções que não são açúcar, cada uma com o motivo
tipostypechecker.pyerros e avisos — e esta fase atravessa os adopt
mirmir.pycorpos e blocos básicos, com arestas rotuladas
analisesmir.pybloco morto, nome talvez não definido, constante provada
ssassa.pynós φ das junções
otimizarotimizar.pyoportunidades de dobra, ramo morto e inalcançável
lirlir.pyquantos nós desceram para fechamento, e quantos recuaram
execucaointerpreter.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:

FaseFatiaO que ela achou
otimizar35,3%8 oportunidades, em 2 dos 3 passes
mir15,4%21 corpos, 66 blocos
tipos11,7%0 erros, 1 aviso
ssa10,9%10 nós φ
lir3,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 desenhoAquiPor quê
Borrow + Dataflow Checkero Dataflow existe; o Borrow, nãonum mundo com coletor, o que se protege é o protocolo — e quem o protege é Arcane.Posse mais três códigos do check
LLVM Backendcompilador.py — fechamentoso LLVM tiraria a zero dependência. Medido: 1,5× a 1,8×, com teto ~6,5×
Machine Code / WASMnão há; o artefato é a árvore compiladarodar em WASM funciona pelo Pyodide, e isso é outra frase
Bare-Metal Runtimenão háo runtime é o CPython
(ausente no desenho) Execution Engineinterpreter.py — o centroo desenho supõe compilação antecipada, e por isso não tem onde pôr o interpretador