Otimizando seu JavaScript no V8

William Grasel

Desafio:

Calcular os 25 mil primeiros números primos!

Quem se importa com otimização de código?

Você deveria!

Não é apenas sobre ser mais rápido...

É sobre viabilizar coisas impossíveis antes!

Classes Ocultas

  • Adivinhar tipos em tempo de execução é demasiadamente custoso.
  • V8 cria classes ocultas em tempo de execução para rastrear o tipo exato de cada objeto.
  • Objetos com a mesma classe oculta tira proveito das mesmas otimizações de código. 

Exemplo:


  function Point(x, y) {  
    this.X = x;
    this.Y = y;
  }

  function Point(x, y) {  
    this.X = x;
    this.Y = y;
  }

  var p1 = new Point(11, 22);

  function Point(x, y) {  
    this.X = x; // classe oculta A criada
    this.Y = y;
  }

  var p1 = new Point(11, 22);

  function Point(x, y) {  
    this.X = x; // classe oculta A criada
    this.Y = y; // classe oculta B criada
  }

  var p1 = new Point(11, 22);

  function Point(x, y) {  
    this.X = x; // classe oculta A criada
    this.Y = y; // classe oculta B criada
  }

  var p1 = new Point(11, 22); // usando classe oculta B

  function Point(x, y) {  
    this.X = x; // classe oculta A criada
    this.Y = y; // classe oculta B criada
  }

  var p1 = new Point(11, 22); // usando classe oculta B
  var p2 = new Point(33, 44);

  function Point(x, y) {  
    this.X = x; // classe oculta A criada
    this.Y = y; // classe oculta B criada
  }

  var p1 = new Point(11, 22); // usando classe oculta B
  var p2 = new Point(33, 44); // usando classe oculta B

  function Point(x, y) {  
    this.X = x; // classe oculta A criada
    this.Y = y; // classe oculta B criada
  }

  var p1 = new Point(11, 22); // usando classe oculta B
  var p2 = new Point(33, 44); // usando classe oculta B

  p1.Z = 55;

  function Point(x, y) {  
    this.X = x; // classe oculta A criada
    this.Y = y; // classe oculta B criada
  }

  var p1 = new Point(11, 22); // usando classe oculta B
  var p2 = new Point(33, 44); // usando classe oculta B

  p1.Z = 55; // classe oculta C criada  

  function Point(x, y) {  
    this.X = x; // classe oculta A criada
    this.Y = y; // classe oculta B criada
  }

  var p1 = new Point(11, 22); // usando classe oculta B
  var p2 = new Point(33, 44); // usando classe oculta B

  p1.Z = 55; // classe oculta C criada
  // p1 e p2 agora usam classes ocultas diferentes!
Point A
X
Point B
X
Y
Point C
X
Y
Z

Atenção!

  • Inicialize todos os seus atributos em uma função construtora.
  • Inicialize todos seus atributos sempre na mesma ordem.
  • Não delete atributos de seus objetos.

Construa objetos previsíveis

Números

  • Inteiros pequenos de 31 bits são especialmente otimizados e devem ser utilizados sempre que possível.
  • Quando um número ultrapassa a barreira de 31 bits ele será tratado como qualquer outro objeto.
  • Essa mudança causa realocação e pode mudar a classe oculta de outro objeto que esteja conectado.

Arrays

  • Elementos Rápidos: armazenamento linear para chaves compactas.
  • Elementos Dicionário: armazenamento em hash table.

Servidos em dois sabores:

Atenção!

  • Dê preferência a arrays do tipo rápido.
  • Use índices numéricos contínuos a partir de 0.
  • Não pré-aloque arrays muito grandes com mais de 64 mil itens.
  • Não delete itens da array diretamente, use Array.splice quando preciso.
  • Não acesse índices inexistentes ou deletados do array.

Classes Ocultas para Arrays

1 - Apenas Inteiros pequenos.

2 - Todo tipo de número.

3 - Objetos em gerais.

Exemplo


  let a = new Array();

  let a = new Array();
  // Nada alocado, assumindo Classe Oculta de Inteiros

  let a = new Array();
  // Nada alocado, assumindo Classe Oculta de Inteiros

  a[0] = 77;   // Primeira alocação

  let a = new Array();
  // Nada alocado, assumindo Classe Oculta de Inteiros

  a[0] = 77;   // Primeira alocação
  a[1] = 88;

  let a = new Array();
  // Nada alocado, assumindo Classe Oculta de Inteiros

  a[0] = 77;   // Primeira alocação
  a[1] = 88;
  a[2] = 0.5;  // Mudança de Classse Oculta... Realoca

  let a = new Array();
  // Nada alocado, assumindo Classe Oculta de Inteiros

  a[0] = 77;   // Primeira alocação
  a[1] = 88;
  a[2] = 0.5;  // Mudança de Classse Oculta... Realoca
  a[3] = true; // Mudança de Classse Oculta... Realoca

JavaScript

Duplamente Compilado

  • O compilador "Completo": compilador inicial que gera um código genérico bom para executar qualquer JavaScript.
  • O compilador Otimizador: que produz um código ainda melhor, mas que demora mais para compilar.

Compilador Completo

  • Igual ao Jon Snow, ele não sabe nada sobre os tipos de dado em tempo de compilação.
  • Utiliza Inline Caches que aprendem sobre seu código em tempo de excução.
  • ICs começam a presumir os tipos de dados, mas sempre checando as Classes Ocultas atuais.
  • Técnica capaz de otimizar a execução do código em algumas centenas de vezes, em tempo de execução.
  • Mudanças constantes de Classes Ocultas impedem que as otimizações sejam criadas e utilizadas.

Compilador Otimizador

  • Funções "quentes", mais utilizadas, são recompiladas pelo segundo compilador.
  • Utiliza informações de tipos obitidos pelos Inline Caches.
  • Especulações dos tipos de dados não são mais checados.
  • O código gerado por esse segundo compilador pode chegar a ser 2x mais rápido que o anterior.

Veja com seus próprios olhos!


  // Veja pela linha de comando o que foi otimizado:
  $ node --trace-opt primes.js

  // Execute o Chrome pela linha de comando para ver o mesmo:
  $ "/Applications/.../Google Chrome" --js-flags="--trace-opt"

Nem tudo pode ser otimizado... ainda

  • Algumas features da própria linguagem podem impedir otimizações, como debugger's e blocos de 'try/catch'.
  • Novas features do ES6/ES2015 podem já estar funcionando, mas impedir otimizações avançadas.

Identifique o que não esta sendo otimizado:


  $ node --trace-bailout primes.js

Separe o código que pode ser otimizado:


  function codigoPerformatico() {  
    // coloque aqui o código que pode ser otimizado
  }

  try {  
    codigoPerformatico()
  } catch (e) { }

Desotimização

  • O Segundo compilador trabalha com especulações, e elas podem falhar...
  •  O código voltará ao seu estado anterior ao compilador otimizado, processo que por si só pode tomar algum tempo e provocar uma lentidão extra.
  • O compilador otimizado poderá voltar novamente para o trecho desotimizado no futuro para uma nova tentativa.

Para rastrear desotimizações:


  $ node --trace-deopt primes.js

Voltando para o Desafio!

Performance Checklist

  • Esteja preparado, conheça o ecossistema que esta trabalhando.
  • Colha métricas e dados confiáveis de execução.
  • Encontre a raiz do problema.
  • Arrume o que for necessário!

Referencias

Perguntas?

Obrigado! =)