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 JavaScriptO tipo de valor pode ser determinado usando o operador
typeof
, mas há uma exceção importante:
typeof 42;
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 indefinidosSeguindo 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' é verdadeiraRepresentaçã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;
Existem várias maneiras de representar números inteiros como 42 na memória:
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];
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) {
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;
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.
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
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,
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 valoresSuponha que estamos executando o seguinte código JavaScript:
ox += 10;
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 yHeapNumber
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;
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:
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 HeapNumberPara 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 MutableHeapNumberComo 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 MutableHeapNumberNo 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 MutableHeapNumberPor 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 };
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 SmiPara continuar ...
Caros leitores! Você encontrou problemas de desempenho do JavaScript causados por recursos do mecanismo JS?
