Pular para o conteúdo

O pipeline de otimização, medido

Os três passes que existem, o que cada um tira — e o número honesto: 1,33× numa carga feita para eles, 1,01× em código real.

Uma referência de linguagem compilada descreve o pipeline do LLVM: inlining, vetorização, análise de alias, otimização de programa inteiro. Nada disso existe aqui, e escrever uma função chamada vetorizar que não vetoriza seria pior que não ter nenhuma.

O pipeline desta linguagem é o compilador de fechamentos: a árvore vira funções Python, uma vez. Acima dele há três passes que tiram trabalho antes de o fechamento ser construído.

PasseO que faz
dobra-de-constante2 + 3 * 4 vira 14, na carga e não por volta
ramo-mortoo ramo cuja condição a propagação condicional prova falsa sai da árvore, com o corpo
inalcancavelo que vem depois de um yield, halt ou trigger sai do corpo
dataforge
adopt Arcane.Compilador as K

// duas dobras: '3 * 4' vira 12, e depois '2 + 12' vira 14
assert K.otimizar("x := 2 + 3 * 4\n")["dobra-de-constante"] is 2

// o ramo provado falso sai inteiro
fonte := "x := 1\ngiven x bigger 5:\n    out 1\notherwise:\n    out 2\n"
assert K.otimizar(fonte)["ramo-morto"] bigger 0

// e os passes estao nomeados, com o que cada um faz
assert len(keys(K.passes())) is 3
assert "na carga" in K.passes()["dobra-de-constante"]

O número#

Esta é a parte que interessa, e ela é desconfortável. A escolha do que compilar não foi intuição: saiu do inventário do LIR, que conta, por classe de nó, o que recua para o interpretador de árvore — e separa os recuos dentro de laço.

O inventário, sobre 388 arquivos do repositório
no                           recuos   em laco
Assignment                      229        92     ← v["k"] := x
MorphOperation                   59        59
SkipStatement                    48        46
MembershipOp                    277        23     ← x in xs
TernaryExpression                85        23
CoalesceOp                       90        19     ← a ?? b
UnaryOp                         116        18
TypeofExpression                123        12
SliceAccess                      44         7

Dez desses nós ganharam construtor no compilador de fechamentos. E o resultado medido:

CargaAntesDepoisGanho
feita dos nós que o inventário aponta507 ms382 ms1,33×
59 exercícios reais do repositório1150 ms1143 ms1,01× — nada

Por isso os três passes ficam desligados por padrão. Eles existem para serem medidos, para dataforge ir --fase=otimizado mostrar o que dá para tirar, e porque a conta honesta é a informação — não a promessa de velocidade.

Duas regras ao mexer nos passes#

A prova é a saída. Um passe errado não levanta erro: ele muda o resultado. Os testes rodam exercícios do repositório nas duas formas e comparam caractere por caractere — a mesma trava do HIR, pelo mesmo motivo.

Nada que possa falhar é dobrado. É a regra que mais recusa:

dataforge
// '1 / 0' dobrado moveria o erro para a CARGA, longe da linha
// que o causa. Sem dobrar, ele estoura onde esta escrito:
monitor:
    x := 1 / 0
handle Error as e:
    assert e.type is "DivisionByZeroError"
    assert e.line is 4
Não dobraPorque
1 / 0, 5 % 0moveria o erro para a carga
"a" + 1mudaria a mensagem de erro
2 ** 1000000meio milhão de dígitos montados no carregamento
a + b com nomeo valor pode não ser o que parece; quem prova isso é o SSA, não a dobra
yes + 1booleano somando é um acidente, não uma conta