Como funciona o JavaScript: dentro do motor V8 + 5 dicas sobre como escrever código otimizado

Ago 21, 2017 · 11 min de leitura

Algumas semanas atrás nós começamos uma série que visa aprofundar-se mais no JavaScript e como ele realmente funciona: nós pensamos que, ao conhecer os blocos de construção de JavaScript e como eles vêm para jogar juntos, você será capaz de escrever código melhor e aplicações.,

A primeira postagem da série focada em fornecer uma visão geral do motor, o tempo de execução e a pilha de chamadas. Este segundo post será mergulhar nas partes internas do motor JavaScript V8 do Google. Nós também forneceremos algumas dicas rápidas sobre como escrever melhor código JavaScript —melhores práticas que nossa equipe de desenvolvimento da SessionStack segue ao construir o produto.

visão geral

um motor JavaScript é um programa ou um interpretador que executa o código JavaScript., Um motor JavaScript pode ser implementado como um interpretador padrão, ou compilador just-in-time que compila JavaScript para bytecode de alguma forma.,/li>

  • SpiderMonkey — o primeiro motor de JavaScript, que em dias alimentado Netscape Navigator, e hoje poderes Firefox
  • JavaScriptCore — fonte aberto, comercializado como Nitro e desenvolvido pela Apple para o Safari
  • KJS — KDE motor originalmente desenvolvido por Harri Porten para o projeto KDE do Konqueror web browser
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Borda
  • Nashorn, de fonte aberta como parte do OpenJDK, escrito pela Oracle Java Idiomas e Grupo de ferramentas
  • JerryScript — é um leve motor para a Internet das Coisas.,
  • por que o motor V8 foi criado?

    o motor V8 que é construído pelo Google é de código aberto e escrito em C++. Este motor é usado dentro do Google Chrome. Ao contrário do resto dos motores, no entanto, V8 também é usado para o nó popular.js runtime.

    V8 foi o primeiro projetado para aumentar o desempenho da execução do JavaScript dentro de navegadores da web., A fim de obter velocidade, V8 traduz código JavaScript em código máquina mais eficiente em vez de usar um interpretador. Ele compila código JavaScript em código de máquina na execução, implementando um compilador JIT (Just-In-Time) como um monte de motores JavaScript modernos fazem como SpiderMonkey ou Rhino (Mozilla). A principal diferença aqui é que V8 não produz bytecode ou qualquer código intermediário.

    V8 usado para ter dois Compiladores

    antes da versão 5.,9 of V8 came out (released earlier this year), the engine used two compilers:

    • full-codegen — a simple and very fast compiler that produced simple and relatively slow machine code.
    • Cambota – um compilador mais complexo (Just-In-Time) otimizando que produziu código altamente otimizado.,l>
    • O thread principal não é o que você esperaria: buscar o seu código, compilá-lo e executá-lo
    • Há também um thread separado para compilar, para que o thread principal pode manter em execução enquanto a primeira é a otimização de código
    • Um Profiler thread que vai dizer o tempo de execução em que os métodos de nós gastamos um monte de tempo para que a Cambota podem otimizá-los
    • algumas threads para lidar com Coletor de Lixo varre

    Quando a primeira-a execução de código JavaScript V8 aproveita completo codegen que se traduz diretamente analisada JavaScript em código de máquina, sem qualquer transformação., Isso permite que ele comece a executar o código da máquina muito rápido. Note que V8 não usa representação intermediária bytecode desta forma removendo a necessidade de um interpretador.

    Quando o seu código está em execução há algum tempo, a thread do profiler recolheu dados suficientes para dizer qual o método que deve ser otimizado.

    A seguir, as otimizações do virabrequim começam em outra linha. Ele traduz a árvore de sintaxe abstrata de JavaScript para uma representação estática de alto nível chamada hidrogênio e tenta otimizar esse grafo de hidrogênio. A maioria das otimizações são feitas neste nível.,

    Inlining

    The first optimization is inlining as much code as possible in advance. Inlining é o processo de substituição de um site de chamada (a linha de código onde a função é chamada) com o corpo da função chamada. Este passo simples permite que seguir otimizações seja mais significativo.

    Oculto classe

    o JavaScript é uma linguagem baseada em protótipos: não existem classes e objetos são criados usando um processo de clonagem., JavaScript é também uma linguagem de programação dinâmica que significa que as propriedades podem ser facilmente adicionadas ou removidas de um objeto após sua instanciação.

    a maioria dos intérpretes JavaScript usam estruturas como dicionário (baseado na função hash) para armazenar a localização dos valores de propriedade do objeto na memória. Esta estrutura torna a recuperação do valor de uma propriedade em JavaScript mais computacionalmente cara do que seria em uma linguagem de programação Não-dinâmica como Java ou C#., Em Java, todas as propriedades do objeto são determinadas por um layout de objeto fixo antes da compilação e não podem ser adicionadas ou removidas dinamicamente em tempo de execução (bem, C# tem o tipo dinâmico que é outro tópico). Como resultado, os valores de Propriedades (ou ponteiros para essas propriedades) podem ser armazenados como um buffer contínuo na memória com um deslocamento fixo entre cada um. O comprimento de um deslocamento pode ser facilmente determinado com base no tipo de propriedade, enquanto que isso não é possível em JavaScript onde um tipo de propriedade pode mudar durante o tempo de execução.,

    Uma vez que usar dicionários para encontrar a localização das propriedades do objeto na memória é muito ineficiente, V8 usa um método diferente em vez disso: classes ocultas. Classes ocultas funcionam da mesma forma que os layouts de objetos fixos (classes) usados em linguagens como Java, exceto que eles são criados em tempo de execução. Agora, vamos ver como eles realmente se parecem:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);

    Uma vez que o “novo ponto(1, 2)” invocação acontece, V8 irá criar uma classe escondida chamada “C0”.,

    propriedades foram definidas para o Ponto ainda, assim, “C0” é vazio.

    Once the first statement “this.x = x “é executado (dentro da função “Ponto”), V8 irá criar uma segunda classe escondida chamada” C1 “que é baseada em”C0”. “C1″ descreve a localização na memória (em relação ao ponteiro do objeto) onde a propriedade x pode ser encontrada., Neste caso, ” x “é armazenado no deslocamento 0, o que significa que ao ver um objeto ponto na memória como um buffer contínuo, o primeiro deslocamento corresponderá à propriedade”x”. V8 também irá atualizar ” C0 “com uma” transição de classe “que afirma que se uma propriedade” x “é adicionada a um objeto ponto, a classe escondida deve mudar de” C0 “para”C1”. A classe escondida para o objeto ponto abaixo é agora “C1”.,

    Cada vez que uma nova propriedade é adicionada a um objeto, o antigo escondido classe é atualizado com um caminho de transição para o novo ocultos classe. Transições de classe ocultas são importantes porque permitem que classes ocultas sejam compartilhadas entre objetos que são criados da mesma forma., Se dois objetos compartilham uma classe oculta e a mesma propriedade é adicionada a ambos, transições irão garantir que ambos os objetos recebem a mesma nova classe oculta e todo o código otimizado que vem com ele.

    este processo é repetido quando a declaração “isto.y = y “é executado (novamente, dentro da função de ponto, após o” isto.x = x” statement).,

    uma nova classe oculta chamada “C2” é criada, uma transição de classe é adicionada a “C1” afirmando que se uma propriedade “y” é adicionada a um objeto pontual (que já contém a propriedade “x”), então a classe oculta deve mudar para “C2”, e a classe oculta do objeto pontual é atualizada para “C2”.

    Oculto classe transições são dependente da ordem em que as propriedades são adicionados a um objeto., Dê uma olhada no excerto de código abaixo:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);
    p1.a = 5;
    p1.b = 6;var p2 = new Point(3, 4);
    p2.b = 7;
    p2.a = 8;

    Agora, você iria assumir que para p1 e p2 as mesmas classes e transições escondidas seriam usadas. Bem, nem por isso. Para “p1″, primeiro a propriedade” A “será adicionada e depois a propriedade”b”. Para” p2″, no entanto, o primeiro” b “está sendo atribuído, seguido por”a”. Assim, ” p1 “e” p2 ” acabam com diferentes classes ocultas como resultado dos diferentes caminhos de transição. Em tais casos, é muito melhor inicializar propriedades dinâmicas na mesma ordem para que as classes ocultas possam ser reutilizadas.,

    caching Inline

    V8 tira partido de outra técnica para otimizar linguagens tipadas dinamicamente chamadas caching inline. Cache Inline baseia-se na observação de que chamadas repetidas para o mesmo método tendem a ocorrer no mesmo tipo de objeto. Uma explicação detalhada do caching em linha pode ser encontrada aqui.

    Vamos tocar no conceito geral de cache em linha (no caso de você não ter tempo para passar pela explicação em profundidade acima).como funciona?, V8 mantém um cache do tipo de objetos que foram passados como um parâmetro em chamadas de método recentes e usa esta informação para fazer uma suposição sobre o tipo de objeto que será passado como um parâmetro no futuro. Se V8 é capaz de fazer uma boa suposição sobre o tipo de objeto que será passado para um método, ele pode contornar o processo de descobrir como acessar as propriedades do objeto, e em vez disso, usar a informação armazenada a partir de pesquisas anteriores para a classe oculta do objeto.como estão os conceitos de classes ocultas e de caching em linha relacionados?, Sempre que um método é chamado em um objeto específico, o motor V8 tem que realizar uma pesquisa para a classe oculta desse objeto, a fim de determinar o deslocamento para acessar uma propriedade específica. Depois de duas chamadas bem sucedidas do mesmo método para a mesma classe escondida, V8 omite a pesquisa de classe escondida e simplesmente adiciona o deslocamento da propriedade para o ponteiro do objeto em si. Para todas as chamadas futuras desse método, o motor V8 assume que a classe escondida não mudou, e salta diretamente para o endereço de memória de uma propriedade específica usando as compensações armazenadas de pesquisas anteriores., Isso aumenta muito a velocidade de execução.

    Cache Inline é também a razão pela qual é tão importante que objetos do mesmo tipo compartilhem classes ocultas. Se você criar dois objetos do mesmo tipo e com escondidas diferentes classes (como fizemos no exemplo anterior), V8 não ser capaz de usar inline cache porque, mesmo que os dois objetos são do mesmo tipo, seu correspondente oculto classes de atribuir diferentes deslocamentos de suas propriedades.,

    Os dois objetos são basicamente o mesmo, mas a “a” e “b” propriedades foram criados em ordem diferente.

    Compilation to machine code

    Uma vez que o grafo de hidrogênio é otimizado, a Cambota baixa para uma representação de nível inferior chamada lítio. A maior parte da implementação do lítio é específica para a arquitetura. A alocação de registro acontece a este nível.,

    no final, o lítio é compilado em código de máquina. Então algo mais acontece chamado OSR: substituição em pilha. Antes de começarmos a compilar e otimizar um método obviamente de longa duração, provavelmente estávamos executando. V8 não vai esquecer o que acabou de executar lentamente para começar de novo com a versão otimizada. Em vez disso, ele vai transformar todo o contexto que temos (pilha, registros) para que possamos mudar para a versão otimizada no meio da execução. Esta é uma tarefa muito complexa, tendo em mente que, entre outras otimizações, o V8 deu ênfase ao código inicialmente., V8 não é o único motor capaz de fazê-lo.

    Existem salvaguardas chamadas de desoptimização para fazer a transformação oposta e reverte de volta para o código não otimizado no caso de uma suposição que o motor feito não se mantém mais verdadeiro.

    coleta de lixo

    para coleta de lixo, V8 usa uma abordagem geracional tradicional de mark-and-sweep para limpar a geração antiga. A fase de marcação deve parar a execução JavaScript., A fim de controlar os custos GC e tornar a execução mais estável, o V8 usa a marcação incremental: em vez de andar todo o heap, tentando marcar todos os objetos possíveis, ele só caminha parte do heap, em seguida, retoma a execução normal. A próxima paragem do GC continuará a partir de onde o heap walk anterior parou. Isto permite pausas muito curtas durante a execução normal. Como mencionado anteriormente, a fase de varredura é tratada por threads separados.

    ignição e TurboFan

    com o lançamento de V8 5.9 no início de 2017, um novo gasoduto de execução foi introduzido., Este novo gasoduto consegue melhorias de desempenho ainda maiores e economias de memória significativas em aplicações JavaScript no mundo real.

    O novo gasoduto de execução é construído no topo da ignição, interpretador de V8, e TurboFan, o mais novo compilador de otimização do V8.

    Você pode conferir o post do blog da equipe V8 sobre o tema aqui.

    desde a versão 5.,9 do V8 saiu, full-codegen e Crankshaft (as tecnologias que têm servido V8 desde 2010) já não têm sido usados pela V8 para a execução JavaScript como a equipe V8 tem lutado para manter o ritmo com as novas características da linguagem JavaScript e as otimizações necessárias para essas características.

    isto significa que o V8 global terá uma arquitetura muito mais simples e mais sustentável indo para a frente.

    Melhorias na Web e no Nó.,js benchmarks

    estas melhorias são apenas o início. O novo gasoduto ignição e TurboFan pavimentam o caminho para novas otimizações que irão aumentar o desempenho JavaScript e encolher a pegada do V8 tanto no cromo quanto Nodo.js nos próximos anos.

    finalmente, aqui estão algumas dicas e truques sobre como escrever bem otimizado, JavaScript melhor., Você pode facilmente calcular estes a partir do conteúdo acima, no entanto, aqui está um resumo para sua conveniência:

    Como escrever otimizada do JavaScript

    1. Ordem das propriedades do objeto: sempre instanciar o objeto de propriedades na mesma ordem, de modo que oculta classes, e, posteriormente, código otimizado, pode ser partilhado.
    2. propriedades dinâmicas: adicionar propriedades a um objecto após instanciação irá forçar uma mudança de classe oculta e abrandar quaisquer métodos que tenham sido optimizados para a classe escondida anterior. Em vez disso, atribua todas as propriedades de um objeto em seu construtor.,
    3. Métodos: o código que executa o mesmo método repetidamente será executado mais rápido do que o código que executa muitos métodos diferentes apenas uma vez (devido ao cache inline).
    4. Arrays: evite arrays esparsos onde as chaves não são números incrementais. Arrays esparsos que não têm todos os elementos dentro deles são uma mesa de hash. Elementos em tais arrays são mais caros de acesso. Além disso, tente evitar pré-alocação de grandes matrizes. É melhor crescer à medida que se vai. Finalmente, não apague elementos em matrizes. Torna as chaves esparsas.
    5. valores marcados: V8 representa objectos e números com 32 bits., Ele usa um bit para saber se ele é um objeto (bandeira = 1) ou um inteiro (bandeira = 0) chamado SMI (pequeno inteiro) por causa de seus 31 bits. Então, se um valor numérico é maior que 31 bits, V8 irá boxear o número, transformando-o em um duplo e criando um novo objeto para colocar o número dentro. Tente usar números assinados de 31 bits sempre que possível para evitar a operação de boxe caro em um objeto JS.

    We at SessionStack try to follow these best practices in writing highly optimized JavaScript code., A razão é que uma vez que você integre SessionStack em seu aplicativo web de produção, ele começa a gravar tudo: todas as mudanças DOM, interações de usuário, exceções JavaScript, traces de pilha, pedidos de rede falidos, e mensagens de depuração. com o SessionStack, você pode reproduzir os problemas em seus aplicativos web como vídeos e ver tudo o que aconteceu com seu usuário. E tudo isso tem que acontecer sem impacto de desempenho para o seu aplicativo web.
    existe um plano livre que permite que você comece de graça.

    Share

    Deixe uma resposta

    O seu endereço de email não será publicado. Campos obrigatórios marcados com *