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
compilado:
const j = 12
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
- LibUV
- N-API
- Esprima AST generator
- TurboFan docs
- TurboFan JIT
- Native modules
- História do JS
- História do Node.js
- Tipos de elementos no V8
- WHATVG spec para microtasks
- V8 Under the Hood
- Fonte do módulo FS
- Motivos para a criação do TurboFan
- Source do método read_file_context do FS
- Exemplos do V8 Under The Hood
- Internals do Node na lib Crypto
- Microtasks e Macrotasks
- Lauching ignition and turbofan
- Performance no V8
- Entendendo Inline caching
- Sea of Nodes
- Sea of Nodes (explicação)
- Bytecodes no NodeJS
- Entendendo bytecodes no V8
- Lista de Vytecodes do V8
- Como funciona o GC do Node.js
- Gerador de códigos interpretados
- O que são pilhas?
- O que são filas?
- Lista de otimizações
- O que são Static Single Assignments?
- On stack replacement no V8
- Por que o node é tão rápido
- You don't know Node.js
- V8 - A tale of Turbofan
- Dicas de otimização no V8
- V8 Internals
- Como o V8 otimiza o código
- Minhas notas pessoais sobre o V8
- [LIVRO] Node.js Under the Hood
- Descobrindo de-optimizações no Node.js
- Entendendo promises de uma vez por todas
- Engine de Renderização do JS
- Alocação de Memória em Javascript
- Como o JS funciona
- Minhas guidelines para a palestra (primeira versão, incompleta) sobre esse tópico
- How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code
- [VIDEO] High performance JS in V8
- [VIDEO] Ryan Dahl's Introduction to Node.js
- [VIDEO] BlinkOn 6 Day 1 Talk 2: Ignition - an interpreter for V8
- [VIDEO] MNUG 2017.03.23 TurboFan: A new code generation architecture for V8
- [VIDEO] Benedikt Meurer: A Tale of TurboFan: Four years that changed V8 forever
- [VIDEO] Marja Hölttä: Parsing JavaScript - better lazy than eager? | JSConf EU 2017
- [VIDEO] Franziska Hinkelmann: JavaScript engines - how do they even? | JSConf EU 2017
- [VIDEO] TDC 2017 - Stadium: Como o Node funciona por Kirmayr Tomaz (in Portuguese)