Otimização de Código

Frank Coelho de Alcantara -2021  

Origem

O termo Bytecode está relacionado com instruções em Assembly.

Um Bytecode seria apenas o código binário da instrução e seus operadores.

O termo Bytecode foi popularizado pelo projeto Java.

Hoje, o termo Bytecode está mais relacionado ao código intermediário.

Constant Folding

Sempre que existir uma operação entre dois literais, execute esta operação.

Com esta otimização seu código pode ser mais claro.

O programador usa: $$tempo = 30 * MINUTOS\_POR\_DIA; $$

O interpretador vê: $$tempo = 43200;$$

Constant Propagation

Se sabemos que, em um determinado ponto do programa, uma variável tem um valor constante, substituímos a referência à variável por este valor constante.

Não é uma otimização simples, precisamos conhecer o fluxo de dado e encontrar o valor da variável em todos os cenários possíveis.

Fica mais simples se o código estiver na forma de SSA

Copy Propagation

Depois de atribuirmos o valor de $A$ a $B$, a referência de $A$ em $B$ pode ser substituída pelo valor de $A$, até que uma nova atribuição ocorra em $A$ ou $B$.

Não é uma otimização simples, precisamos conhecer o fluxo de dado e encontrar o valor da variável em todos os cenários possíveis.

Fica mais simples se o código estiver na forma de SSA

exemplo de código substituindo multiplicação por somas.

Use Define Chain

Outro grafo relacionando cada definição de variável a todos os usos desta variável.

Computacionalmente muito caro: uma variável com $d$ definições e $u$ usos terá complexidade $O(d\times u)$.

Vamos simplificar este processo definindo cada variável apenas uma vez?

E assim nasceu o SSA dentro da IBM.

Static Single Assignment - SSA

Método para estruturar o código intermediário de forma que cada variável seja atribuída apenas uma vez.

A ideia é remover possíveis redundâncias no uso de variáveis.

Precisamos encontrar todos os pontos onde uma variável é usada.

Outra estrutura de dados: Cadeias de definição de uso def. use chain.

SSA Em Blocos Básicos

permite otimização utilizando as características da linguagem.

SSA Fluxo

Quando voltamos de um $if$, por exemplo, como podemos saber que variável usar?

Infelizmente este é um problema insolúvel. Não há como determinar isso de forma autônoma.

Então, vamos usar uma função alternativa $a3 := \Phi (a1, a2)$ que indica que $a3$ pode receber $a1$ ou $a2$ dependendo do fluxo do programa.

E assim nasceu o SSA dentro da IBM..

SSA Fluxo Exemplo

permite otimização utilizando as características da linguagem.

Simplificação Algébrica

Usamos as regras da álgebra.

Não otimizado: $((4*(a + b))/ (4*a))*c$.

  1. $((4*a + 4*b)/ (4*a)) * c $
  2. $((4*a)/(4*a) + (4*b)/(4*a)) * c$
  3. $(1 + b/a) * c$
  4. $(1*c) + (b/a)*c$
  5. $c + (b*c)/a$

Otimizado: $c + (b*c)/a$. A grande pergunta é: você vai deixar o interpretador fazer isso?

Simplificação Algébrica - Cuidados

Divisão por zero? Overflow?

Verificação de overflow tem impacto direto na interpretação.

GCC 9 clang 9
no trapping 0.17 ns/int 0.11 ns/int
trapping 2.1 ns/int 0.32 ns/int
slowdown 12 x 3 x

Fonte: Daniel Lemire (2020)

Strength Reduction

Algumas operações são computacionalmente mais caras em arquiteturas diferentes.

Infelizmente este é um problema insolúvel. Não há como determinar isso de forma autônoma.

A otimização mais comum é substituir multiplicações por somas.

exemplo de código substituindo multiplicação por somas.

Strength Reduction - shift

$a = b/16 \Rightarrow a = b >> 4$

$a = b*64 \Rightarrow a = b << 6$

$a = b*15 \Rightarrow a = (b<<4) – b$

Dependem do processador e do número que está sendo operado.

Por outro lado, o processo de substituição é simples e direto.

exemplo de código substituindo multiplicação por somas.

Loop Invariant

Remover dos laços todo o código que não está sendo alterado dentro do código.

No exemplo há outra otimização: strength reduction.

exemplo de código substituindo multiplicação por somas.

Lifetime

Verificar e ajustar o uso de variáveis de acordo com seu tempo de vida, ou referências.

Em C++ isso não é fácil.

exemplo de código substituindo multiplicação por somas.

Function inlining

Economiza todo o custo de chamar a função e voltar.

Algumas linguagens possuem indicadores para provocar isso: inline in C++.

Economiza memória no stack, abre espaço para outras otimizações locais.

Precisa tomar cuidado com o excesso de uso.

Unswitching

Não é raro que o processo interno de um laço não seja relacionado ao laço.

Também não é raro que isso possa ser modificado se o laço for modificado..

Economiza memória no stack, abre espaço para outras otimizações locais.

Precisa tomar cuidado com o excesso de uso.

exemplo de código substituindo multiplicação por somas.