Node.js por Baixo dos Panos #10 - Otimizações do Compilador

Lucas Santos - May 21 '20 - - Dev Community

Photo por Michael Dziedzic no Unsplash

Nos artigos anteriores, falamos sobre como o Node.js funciona por baixo dos panos e como o V8 compila o código com tanta eficiência, a maior parte dessa eficiência está relacionada a otimizações do compilador, portanto, neste artigo, finalmente conheceremos o que são e como eles funcionam!

Este é um breve resumo de várias otimizações do compilador que o V8 pode executar no código. O objetivo deste artigo é apenas apresentar quais tipos de coisas estão incluídas quando dizemos "otimização". Não vamos nos aprofundar em como os compiladores fazem isso.

Todas as otimizações abaixo são feitas enquanto o compilador está analisando o código.

On Stack Replacement

On Stack Replacement é a técnica de otimização que substitui um pedaço de código não otimizado por outro pedaço de código otimizado durante a execução. O V8 faz isso sempre que precisa otimizar uma única função ou o código em execução. Em resumo, On Stack Replacement significa que o stack frame atual será substituído por outro stack frame de código otimizado sem perder nenhuma outra informação, enquanto o código ainda está em execução. É como trocar os pneus de um carro no meio de uma corrida, com ele ainda correndo.

Constant Folding

Substitui expressões constantes pelo seu valor final no tempo de compilação, em vez de fazer o cálculo no tempo de execução.

Exemplo:

não compilado:

const j = 3 + 9
Enter fullscreen mode Exit fullscreen mode

compilado:

const j = 12
Enter fullscreen mode Exit fullscreen mode

Análise Indutiva de Variável

Em um loop, se uma variável for uma função linear simples da variável que estamos usando como índice, por exemplo, const p = 4 * i + 1, ela poderá ser atualizada adequadamente cada vez que a variável do loop for alterada.

Isso é chamado de redução de força, uma forma de otimização em que operações caras são substituídas por operações menos caras equivalentes, por exemplo, uma multiplicação cara é substituída por uma série de adições mais baratas.

Portanto o código acima seria substituído por algo como: const p = (i + 1) + (i + 1) + (i + 1) + (i + 1)

Rematerialização

O ato de recalcular o valor de uma variável ao invés de puxar o valor já calculado da memória. Isso evita que a memória seja acessada muitas vezes.

Remoção de Recursão

A recursão geralmente é muito cara, como vimos quando falamos sobre Stack Overflow. Os algoritmos recursivos chamados de Tail Recursion (código que termina retornando uma chamada para si mesmo) podem ser convertidos em algoritmos iterativos, o que elimina os problemas da pilha. Isso geralmente é feito usando Tail Call Optimisations, que é o processo no qual você é capaz de evitar a alocação de um novo stackframe para uma função porque a função que está chamando a nova execução vai simplesmente retornar o valor que a nova execução calcular. Portanto, esta última chamada pode ser substituída pela própria função.

Peephole Optimisations

Essas são geralmente executados no final do processo de compilação, após a geração do código da máquina. Essa técnica de otimização examina algumas instruções adjacentes (como olhar através de uma fechadura, daí o nome peephole) para ver se elas podem ser substituídas por uma única instrução ou uma sequência mais curta de instruções.

Um exemplo é uma multiplicação por uma potência de 2, que pode ser substituída por um deslocamento à esquerda bit a bit. (que também é uma otimização de redução de força).

Expansão Linear

Essa é a técnica de substituir a chamada para uma função pelo seu corpo. Isso economiza muito na hora de adicionar outro stack frame e é também uma grande oportunidade para otimizações específicas de parâmetros, mas isso tem um custo de espaço. Se o método for chamado várias vezes durante um programa, seu corpo será substituído várias vezes, o que pode levar a um código maior e mais pesado.

Geralmente, essa linearidade é muito útil para códigos de desempenho críticos que fazem um grande número de chamadas para procedimentos pequenos, portanto, há menos saltos.

Inline Caching

Inline Caching se baseia na observação de que chamadas repetidas para o mesmo método tendem a ocorrer no mesmo tipo de objeto. A V8 mantém um cache do tipo de objetos que foram passados como parâmetro nas chamadas de método recentes e usa essas informações para fazer uma suposição sobre o tipo de objeto que será passado como parâmetro no futuro. Se essa suposição for boa, a próxima chamada poderá ignorar o processo de descobrir como acessar as propriedades do objeto e, em vez disso, usar as informações armazenadas de pesquisas prévias nas hidden classes (classes ocultas) desse objeto.

Isso se refere especificamente ao conceito de hidden classes porque, sempre que um método é chamado em um objeto específico, o mecanismo deve procurar a hidden class a fim de encontrar o offset de memória para essa propriedad. Após duas chamadas bem-sucedidas desse mesmo método para a mesma classe oculta, a V8 omite a pesquisa de classe oculta e adiciona o deslocamento a essa propriedade no próprio ponteiro de objeto. Isso aumenta muito a velocidade de execução.

Eliminação de "Código Morto"

Esse processo elimina o código que nunca é chamado no programa. Ele faz isso, explicando por cima, passando por todos os bytecodes durante a execução do programa, gera um gráfico e elimina as partes que não pertencem a nenhum caminho de código.

Reordenação de código

A reordenação de bloco de código altera a ordem dos blocos básicos em um programa para reduzir ramificações condicionais e melhorar a "localidade de referência", que é a tendência de um processador acessar o mesmo conjunto de locais de memória repetidamente por um curto período de tempo.

Jump Threading

Saltos condicionais consecutivos baseados total ou parcialmente na mesma condição podem ser mesclados. Por exemplo: if (c) { foo; } if (c) { bar; } vira if (c) { foo; bar; }

Trampolines

Muitas CPUs possuem sub-rotinas menores, instruções de chamada para acessar pouca memória. O compilador pode economizar espaço usando essas pequenas chamadas no corpo da função. Multiplicando a economia de espaço da refatoração de código.

Eliminação de Expressões Comuns

Sempre que repetimos subexpressões, como em (a + b) * 2 + (a + b), a subexpressão comum é a + b. Portanto, o compilador calcula o valor de a + b apenas uma vez e usa constant folding para substituí-lo na chamada da expressão, assumindo que a mesma não mude ao longo do tempo.

Conclusão

Você conseguiu! Você finalmente chegou ao final de nossa série de 10 partes sobre o Node.js por baixo dos panos! Espero que você tenha gostado e tenha se sentido um pouco mais animado para saber mais!

Abaixo, deixarei todas as referências que usei para compor todos esses artigos e também um link para o rascunho do artigo original no meu GitHub. É isso aí! Muito obrigado por ler e me dar um feedback sobre a série :D

Não deixe de acompanhar mais do meu conteúdo no meu blog e se inscreva na newsletter para receber notícias semanais!

Referências

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .