Uma história sobre V8, React e uma queda no desempenho. Parte 1

O material, a primeira parte da tradução publicada hoje, discutirá como o mecanismo JavaScript da V8 seleciona as melhores maneiras de representar vários valores JS na memória e como isso afeta os mecanismos internos da V8 em relação ao trabalho com os chamados formulários. objetos (Forma). Tudo isso nos ajudará a descobrir a essência do recente problema de desempenho do React .



Tipos de dados JavaScript


Cada valor JavaScript pode ter apenas um dos oito tipos de dados existentes: Number , String , Symbol , BigInt , Boolean , Undefined , Null e Object .


Tipos de dados JavaScript

O tipo de valor pode ser determinado usando o operador typeof , mas há uma exceção importante:

 typeof 42; // 'number' typeof 'foo'; // 'string' typeof Symbol('bar'); // 'symbol' typeof 42n; // 'bigint' typeof true; // 'boolean' typeof undefined; // 'undefined' typeof null; // 'object' -   ,     typeof { x: 42 }; // 'object' 

Como você pode ver, o comando typeof null retorna 'object' , não 'null' , apesar do fato de o null ter seu próprio tipo - Null . Para entender o motivo desse typeof comportamento, levamos em consideração o fato de que o conjunto de todos os tipos de JavaScript pode ser dividido em dois grupos:

  • Objetos (ou seja, digite Object ).
  • Valores primitivos (ou seja, quaisquer valores não-objetivos).

À luz desse conhecimento, verifica-se que null significa "sem valor de objeto", enquanto undefined significa "sem valor".


Valores primitivos, objetos, nulos e indefinidos

Seguindo essas reflexões no espírito de Java, Brendan Eich projetou o JavaScript para que o operador typeof retornasse 'object' para os valores desses tipos que estão localizados na figura anterior à direita. Todos os valores de objetos e null chegam aqui. É por isso que a expressão typeof null === 'object' é verdadeira, embora exista um tipo separado Null na especificação da linguagem.


A expressão typeof v === 'object' é verdadeira

Representação de valores


Os mecanismos JavaScript devem poder representar quaisquer valores JavaScript na memória. No entanto, é importante observar que os tipos de valor em JavaScript são separados de como os mecanismos JS os representam na memória.

Por exemplo, um valor de 42 no JavaScript é do tipo number .

 typeof 42; // 'number' 

Existem várias maneiras de representar números inteiros como 42 na memória:
Submissão
Bits
8 bits, além de dois
0010 1010
32 bits, com adição de até dois
0000 0000 0000 0000 0000 0000 0010 1010
Decimal com código binário (BCD) compactado
0100 0010
32 bits, número de ponto flutuante IEEE-754
0 100 0010 0010 1000 0000 0000 0000 0000
64 bits, número de ponto flutuante IEEE-754
0100 0000 0100 0101 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

De acordo com o padrão ECMAScript, os números são valores de ponto flutuante de 64 bits, conhecidos como números de ponto flutuante de precisão dupla (Float64). No entanto, isso não significa que os mecanismos JavaScript sempre armazenem números em uma exibição do Float64. Isso seria muito, muito ineficiente! Os mecanismos podem usar outras representações internas de números - desde que o comportamento dos valores corresponda exatamente ao comportamento dos números do Float64.

A maioria dos números em aplicativos JS reais, como se viu, são índices de matriz ECMAScript válidos. Ou seja - números inteiros no intervalo de 0 a 2 32 -2.

 array[0]; //      . array[42]; array[2**32-2]; //      . 

Os mecanismos JavaScript podem escolher o formato ideal para representar esses valores na memória. Isso é feito para otimizar o código que funciona com elementos da matriz usando índices. Um processador que executa operações de acesso à memória precisa que os índices da matriz estejam disponíveis como números armazenados em uma exibição com a adição de dois . Se, em vez disso, representarmos os índices de matrizes na forma de valores do Float64, isso significaria um desperdício de recursos do sistema, pois o mecanismo precisaria converter os números do Float64 em um formato com adição de dois e vice-versa sempre que alguém acessar um elemento da matriz.

A representação de números de 32 bits com a adição de até dois é útil não apenas para otimizar o trabalho com matrizes. Em geral, pode-se notar que o processador executa operações inteiras muito mais rapidamente do que as operações que usam valores de ponto flutuante. É por isso que, no exemplo a seguir, o primeiro ciclo sem problemas é duas vezes mais rápido em comparação com o segundo ciclo.

 for (let i = 0; i < 1000; ++i) {  //  } for (let i = 0.1; i < 1000.1; ++i) {  //  } 

O mesmo se aplica aos cálculos usando operadores matemáticos.

Por exemplo, o desempenho do operador de retirar o restante da divisão do próximo fragmento de código depende de quais números estão envolvidos nos cálculos.

 const remainder = value % divisor; //  -  `value`  `divisor`   , //    . 

Se os dois operandos são representados por números inteiros, o processador pode calcular o resultado com muita eficiência. Há otimização adicional no V8 para casos em que o operando do divisor é representado por um número com potência de dois. Para valores representados como números de ponto flutuante, os cálculos são muito mais complicados e demoram muito mais.

Como as operações com números inteiros geralmente são executadas muito mais rapidamente do que as operações com valores de ponto flutuante, pode parecer que os mecanismos sempre podem armazenar todos os números inteiros e todos os resultados de operações com números inteiros em um formato com adição de dois. Infelizmente, essa abordagem violaria a especificação ECMAScript. Como já mencionado, o padrão fornece a representação de números no formato Float64, e algumas operações com números inteiros podem levar ao aparecimento de resultados na forma de números de ponto flutuante. É importante que, nessas situações, os mecanismos JS produzam resultados corretos.

 //  Float64   53-  . //         . 2**53 === 2**53+1; // true // Float64   ,   -1 * 0   -0,  //           . -1*0 === -0; // true // Float64   Infinity,   , //     . 1/0 === Infinity; // true -1/0 === -Infinity; // true // Float64    NaN. 0/0 === NaN; 

Embora no exemplo anterior todos os números no lado esquerdo das expressões sejam números inteiros, todos os números no lado direito das expressões são valores de ponto flutuante. É por isso que nenhuma das operações anteriores pode ser executada corretamente usando um formato de 32 bits com uma adição de até duas. Os mecanismos JavaScript precisam prestar atenção especial para garantir que, ao executar operações com números inteiros, você obtenha os resultados Float64 corretos (embora capazes de parecer incomuns - como no exemplo anterior).

No caso de números inteiros pequenos que se enquadram no intervalo da representação de 31 bits de números inteiros assinados, o V8 usa uma representação especial chamada Smi . Tudo o que não é um valor Smi é representado como um valor HeapObject , que é o endereço de alguma entidade na memória. Para números que não se enquadram no intervalo Smi , temos um tipo especial de HeapObject - o chamado HeapNumber .

 -Infinity // HeapNumber -(2**30)-1 // HeapNumber  -(2**30) // Smi       -42 // Smi        -0 // HeapNumber         0 // Smi       4.2 // HeapNumber        42 // Smi   2**30-1 // Smi     2**30 // HeapNumber  Infinity // HeapNumber       NaN // HeapNumber 

Como você pode ver no exemplo anterior, alguns números JS são representados como Smi e outros como HeapNumber . O mecanismo V8 é otimizado em termos de processamento de números Smi . O fato é que pequenos números inteiros são muito comuns em programas JS reais. Ao trabalhar com valores Smi , não é necessário alocar memória para entidades individuais. Além disso, seu uso permite executar operações rápidas com números inteiros.

Comparação de Smi, HeapNumber e MutableHeapNumber


Vamos falar sobre como é a estrutura interna desses mecanismos. Suponha que tenhamos o seguinte objeto:

 const o = {  x: 42, // Smi  y: 4.2, // HeapNumber }; 

O valor 42 da propriedade do objeto x é codificado como Smi . Isso significa que ele pode ser armazenado dentro do próprio objeto. Para armazenar o valor 4.2, por outro lado, você precisará criar uma entidade separada. No objeto, haverá um link para esta entidade.


Armazenamento de vários valores

Suponha que estamos executando o seguinte código JavaScript:

 ox += 10; // ox   52 oy += 1; // oy   5.2 

Nesse caso, o valor da propriedade x pode ser atualizado em seu local de armazenamento. O fato é que o novo valor de x é 52 e esse número está dentro do intervalo de Smi .


O novo valor da propriedade x é armazenado onde o valor anterior foi armazenado.

No entanto, o novo valor de y , 5.2, não se encaixa no intervalo de Smi e, além disso, difere do valor anterior de y - 4.2. Como resultado, a V8 precisa alocar memória para a nova entidade HeapNumber e referenciá-la já do objeto.


Nova entidade HeapNumber para armazenar o novo valor y

HeapNumber entidades HeapNumber são imutáveis. Isso permite implementar algumas otimizações. Suponha que desejemos definir a propriedade do objeto x valor da propriedade y :

 ox = oy; // ox   5.2 

Ao executar esta operação, podemos simplesmente nos referir à mesma entidade HeapNumber e não alocar memória adicional para armazenar o mesmo valor.

Uma das desvantagens da imunidade das entidades HeapNuber é que a atualização frequente de campos com valores fora do intervalo Smi é lenta. Isso é demonstrado no seguinte exemplo:

 //   `HeapNumber`. const o = { x: 0.1 }; for (let i = 0; i < 5; ++i) {  //    `HeapNumber`.  ox += 1; } 

Ao processar a primeira linha, é criada uma instância de HeapNumber , cujo valor inicial é 0.1. No corpo do ciclo, esse valor muda para 1.1, 2.1, 3.1, 4.1 e, finalmente, para 5.1. Como resultado, no processo de execução desse código, HeapNumber 6 instâncias do HeapNumber , cinco das quais serão submetidas a operações de coleta de lixo após a conclusão do loop.


Entidades HeapNumber

Para evitar esse problema, a V8 possui otimização, que é um mecanismo para atualizar campos numéricos cujos valores não se enquadram no intervalo Smi nos mesmos locais em que já estão armazenados. Se um campo numérico armazena valores para os quais a entidade Smi não Smi adequada para armazenamento, a V8, na forma de um objeto, marca esse campo como Double e aloca memória para a entidade MutableHeapNumber , que armazena o valor real representado no formato Float64.


Usando entidades MutableHeapNumber

Como resultado, após a alteração do valor do campo, a V8 não precisa mais alocar memória para a nova entidade HeapNumber . Em vez disso, basta escrever o novo valor em uma entidade MutableHeapNumber existente.


Escrevendo um novo valor para MutableHeapNumber

No entanto, essa abordagem tem suas desvantagens. Ou seja, como os valores de MutableHeapNumber podem mudar, é importante garantir que o sistema funcione de maneira que esses valores se comportem conforme fornecido na especificação de idioma.


Desvantagens do MutableHeapNumber

Por exemplo, se você atribuir o valor de ox alguma outra variável y , precisará garantir que o valor de y não seja alterado com uma mudança subsequente em ox . Isso seria uma violação da especificação do JavaScript! Como resultado, ao acessar ox , o número deve ser reembalado para o valor HeapNumber usual antes de ser atribuído y .

No caso de números de ponto flutuante, o V8 executa as operações de empacotamento acima usando seus mecanismos internos. Porém, no caso de números inteiros pequenos, usar MutableHeapNumber seria uma perda de tempo, porque Smi é uma maneira mais eficiente de representar esses números.

 const object = { x: 1 }; // ""  `x`    object.x += 1; //   `x`   

Para evitar o uso ineficiente dos recursos do sistema, tudo o que precisamos fazer para trabalhar com números inteiros pequenos é marcar os campos correspondentes nas formas de objetos como Smi . Como resultado, os valores desses campos, desde que correspondam ao intervalo Smi , podem ser atualizados diretamente dentro dos objetos.


Trabalhar com números inteiros cujos valores se enquadram no intervalo de Smi

Para continuar ...

Caros leitores! Você encontrou problemas de desempenho do JavaScript causados ​​por recursos do mecanismo JS?

Source: https://habr.com/ru/post/pt467247/


All Articles