Hoje estamos publicando a segunda parte da tradução do material dedicado aos mecanismos internos do V8 e a investigação do problema de desempenho do React.

→
A primeira parteObsolescência e migração de formas de objetos
E se o campo contivesse inicialmente um
Smi
e a situação mudasse e fosse necessário armazenar um valor para o qual a representação
Smi
não
Smi
adequada? Por exemplo, como no exemplo a seguir, quando dois objetos são representados usando a mesma forma do objeto em que
x
inicialmente armazenado como
Smi
:
const a = { x: 1 }; const b = { x: 2 };
No início do exemplo, temos dois objetos, cuja representação usamos a mesma forma do objeto em que o formato
Smi
é usado para armazenar
x
.
O mesmo formulário é usado para representar objetosQuando a propriedade
bx
alterada e é necessário usar o formato
Double
para representá-la, o V8 aloca espaço de memória para a nova forma do objeto, na qual
x
é atribuída a representação
Double
e que indica um formulário vazio. A V8 também cria uma entidade
MutableHeapNumber
, usada para armazenar o valor 0.2 da propriedade
x
. Em seguida, atualizamos o objeto
b
para que ele se refira a esse novo formulário e
MutableHeapNumber
o slot no objeto para que ele
MutableHeapNumber
à entidade
MutableHeapNumber
criada anteriormente no deslocamento 0. Finalmente, marcamos a forma antiga do objeto como obsoleta e a desconectamos da árvore transições. Isso é feito criando uma nova transição para
'x'
do formulário vazio para o que acabamos de criar.
Consequências da atribuição de um novo valor a uma propriedade de objetoNo momento, não podemos excluir completamente o formulário antigo, pois ele ainda é usado pelo objeto
a
. Além disso, será muito caro ignorar toda a memória na pesquisa de todos os objetos que se referem ao formulário antigo e atualizar imediatamente o estado desses objetos. Em vez disso, o V8 usa uma abordagem "preguiçosa" aqui. Ou seja, todas as operações de leitura ou gravação das propriedades do objeto
a
primeiro transferidas para o uso de um novo formulário. A idéia por trás dessa ação é, no final das contas, tornar a forma obsoleta do objeto inatingível. Isso fará com que o coletor de lixo lide com isso.
A memória fora de forma libera o coletor de lixoAs coisas são mais complicadas em situações em que o campo que altera a visualização não é o último da cadeia:
const o = { x: 1, y: 2, z: 3, }; oy = 0.1;
Nesse caso, o V8 precisa encontrar a chamada forma de divisão. Este é o último formulário da cadeia, localizado antes do formulário em que a propriedade correspondente aparece. Aqui nós mudamos
y
, isto é - precisamos encontrar a última forma em que não havia
y
. No nosso exemplo, esse é o formato no qual
x
aparece.
Pesquise o último formulário em que não houve valor alteradoAqui, começando com este formulário, criamos uma nova cadeia de transição para
y
que reproduz todas as transições anteriores. Somente agora a propriedade
'y'
será representada como um
Double
. Agora usamos essa nova cadeia de transição para
y
, marcando-a como uma subárvore antiga obsoleta. Na última etapa, migramos a instância do objeto
o
para um novo formulário, agora usando a entidade
MutableHeapNumber
para armazenar o
MutableHeapNumber
y
. Com essa abordagem, o novo objeto não usará fragmentos da árvore de transição antiga e, depois que todas as referências à forma antiga desaparecerem, a parte obsoleta da árvore também desaparecerá.
Extensibilidade e integridade de transição
O comando
Object.preventExtensions()
permite impedir completamente a adição de novas propriedades a um objeto. Se você processar o objeto com este comando e tentar adicionar uma nova propriedade a ele, uma exceção será lançada. (Verdadeiro, se o código não for executado no modo estrito, uma exceção não será lançada, no entanto, uma tentativa de adicionar uma propriedade simplesmente não causará consequências). Aqui está um exemplo:
const object = { x: 1 }; Object.preventExtensions(object); object.y = 2;
O método
Object.seal()
atua nos objetos da mesma maneira que
Object.preventExtensions()
, mas também marca todas as propriedades como não configuráveis. Isso significa que eles não podem ser excluídos, nem suas propriedades podem ser alteradas em relação às possibilidades de listá-las, configurá-las ou reescrevê-las.
const object = { x: 1 }; Object.seal(object); object.y = 2;
O método
Object.freeze()
executa as mesmas ações que
Object.seal()
, mas seu uso, além disso, leva ao fato de que os valores das propriedades existentes não podem ser alterados. Eles são marcados como propriedades nas quais novos valores não podem ser gravados.
const object = { x: 1 }; Object.freeze(object); object.y = 2;
Considere um exemplo específico. Temos dois objetos, cada um com um valor único
x
. Então proibimos a extensão do segundo objeto:
const a = { x: 1 }; const b = { x: 2 }; Object.preventExtensions(b);
O processamento desse código começa com ações que já conhecemos. Ou seja, é feita uma transição da forma vazia do objeto para a nova forma, que contém a propriedade
'x'
(representada como uma entidade
Smi
). Quando proibimos a expansão do objeto
b
, isso leva a uma transição especial para uma nova forma, que é marcada como não expansível. Essa transição especial não leva ao aparecimento de novas propriedades. Isso é, de fato, apenas um marcador.
O resultado do processamento de um objeto usando o método Object.preventExtensions ()Observe que não podemos simplesmente alterar a forma existente com o valor
x
, pois ela é necessária para outro objeto, o objeto
a
, que ainda é expansível.
Reagir problema de desempenho
Agora vamos coletar tudo o que falamos e usar o conhecimento que adquirimos para entender a essência do recente
problema de desempenho do React. Quando a equipe do React analisou aplicativos reais, eles notaram uma degradação estranha no desempenho da V8 que agia no núcleo do React. Aqui está uma reprodução simplificada da parte problemática do código:
const o = { x: 1, y: 2 }; Object.preventExtensions(o); oy = 0.2;
Temos um objeto com dois campos representados como entidades
Smi
. Evitamos uma expansão adicional do objeto e, em seguida, executamos uma ação que leva ao fato de que o segundo campo deve ser representado no formato
Double
.
Já descobrimos que a proibição da expansão do objeto leva aproximadamente à seguinte situação.
Consequências da proibição de expansão de objetosPara representar as duas propriedades do objeto, são
Smi
entidades
Smi
e a última transição é necessária para marcar a forma do objeto como não extensível.
Agora precisamos alterar a maneira como a propriedade
y
é representada por
Double
. Isso significa que precisamos começar a procurar uma forma de separação. Nesse caso, esse é o formato no qual a propriedade
x
aparece. Mas agora o V8 está confuso. O fato é que a forma de separação era extensível e a forma atual foi marcada como não extensível. V8 não sabe como reproduzir o processo de transição em uma situação semelhante. Como resultado, o mecanismo simplesmente se recusa a tentar descobrir tudo. Em vez disso, ele simplesmente cria um formulário separado que não está conectado à árvore de formulários atual e não é compartilhado com outros objetos. Isso é algo como uma forma órfã de um objeto.
Formulário órfãoÉ fácil adivinhar que isso, se isso acontecer com muitos objetos, é muito ruim. O fato é que isso torna todo o sistema de formas de objetos V8 inútil.
Quando ocorreu um problema recente do React, aconteceu o seguinte. Cada objeto da classe
FiberNode
tinha campos destinados a armazenar registros de data e hora quando a criação de perfil está ativada.
class FiberNode { constructor() { this.actualStartTime = 0; Object.preventExtensions(this); } } const node1 = new FiberNode(); const node2 = new FiberNode();
Esses campos (por exemplo,
actualStartTime
) foram inicializados em 0 ou -1. Isso levou ao fato de que as entidades
Smi
foram usadas para representar seus significados
Smi
. Porém, mais tarde, eles salvaram carimbos de tempo real no formato de números de ponto flutuante retornados pelo método
performance.now (). Isso levou ao fato de que esses valores não podiam mais ser representados na forma de
Smi
. Para representar esses campos, agora eram necessárias entidades
Double
. Além disso, o React também impediu a expansão de instâncias da classe
FiberNode
.
Inicialmente, nosso exemplo simplificado pode ser apresentado da seguinte forma.
Estado inicial do sistemaExistem duas instâncias da classe que compartilham a mesma árvore de transições da forma de objetos. A rigor, é para isso que o sistema de formas de objetos do V8 foi projetado. Porém, quando os carimbos de tempo real são armazenados no objeto, o V8 não consegue entender como ele pode encontrar a forma de separação.
V8 está confusoV8 atribui um novo formulário órfão ao
node1
. O mesmo acontece um pouco mais tarde com o objeto
node2
. Como resultado, agora temos duas formas "órfãs", cada uma das quais é usada por apenas um objeto. Em muitos aplicativos React reais, o número desses objetos é muito maior que dois. Estes podem ser dezenas ou mesmo milhares de objetos da classe
FiberNode
. É fácil entender que essa situação não afeta muito o desempenho da V8.
Felizmente,
corrigimos esse problema na
V8 v7.4 e estamos
explorando a possibilidade de tornar a operação de alterar a representação de campos de objetos menos intensiva em recursos. Isso nos permitirá resolver os problemas de desempenho restantes que surgem nessas situações. A V8, graças à correção, agora se comporta corretamente na situação do problema acima descrita.
Estado inicial do sistemaAqui está como fica. Duas instâncias da classe
FiberNode
referência a um formulário não extensível. Nesse caso,
'actualStartTime'
é representado como um campo
Smi
. Quando a primeira operação de atribuição de um valor à propriedade
node1.actualStartTime
é
node1.actualStartTime
, uma nova cadeia de transição é criada e a cadeia anterior é marcada como obsoleta.
Resultados da atribuição de um novo valor à propriedade Node1.actualStartTimeObserve que a transição para o formulário não expansível agora está corretamente reproduzida na nova cadeia. É
node2.actualStartTime
que o sistema entra depois de alterar o valor de
node2.actualStartTime
.
Os resultados da designação de um novo valor à propriedade node2.actualStartTimeDepois que o novo valor é designado à propriedade
node2.actualStartTime
, os dois objetos se referem ao novo formulário e a parte obsoleta da árvore de transição pode ser destruída pelo coletor de lixo.
Observe que as operações para marcar as formas de objetos como obsoletas e sua migração podem parecer algo complicado. De fato - do jeito que está. Suspeitamos que em sites reais isso faça mais mal (em termos de desempenho, uso de memória, complexidade) do que bom. Especialmente - depois, no caso de
compactação de
ponteiro , não podemos mais usar essa abordagem para armazenar campos
Double
na forma de valores incorporados em objetos. Como resultado, esperamos
abandonar completamente o mecanismo de obsolescência das formas de objeto V8 e tornar esse mecanismo obsoleto.
Observe que a equipe do React
resolveu esse problema por conta própria, certificando-se de que os campos nos objetos da classe
FiberNodes
inicialmente representados por valores Double:
class FiberNode { constructor() {
Aqui, em vez de
Number.NaN
, qualquer valor de ponto flutuante que não se ajuste ao intervalo
Smi
pode ser usado. Entre esses valores estão 0,000001,
Number.MIN_VALUE
, -0 e
Infinity
.
Vale ressaltar que o problema descrito no React era específico da V8 e que, ao criar algum código, os desenvolvedores não precisam se esforçar para otimizá-lo com base em uma versão específica de um determinado mecanismo JavaScript. No entanto, é útil poder corrigir algo otimizando o código caso as causas de alguns erros estejam enraizadas nos recursos do mecanismo.
Vale lembrar que, nas entranhas dos motores JS, existem muitos tipos de coisas surpreendentes. O desenvolvedor JS pode ajudar a todos esses mecanismos, se possível, sem atribuir os mesmos valores de variáveis de tipos diferentes. Por exemplo, você não deve inicializar campos numéricos como
null
, pois isso negará todas as vantagens de observar a representação do campo e melhorará a legibilidade do código:
Em outras palavras - escreva código legível, e o desempenho virá por si só!
Sumário
Neste artigo, examinamos os seguintes problemas importantes:
- O JavaScript distingue entre os valores "primitivo" e "objeto", e o
typeof
resultado não pode ser confiável. - Mesmo valores que têm o mesmo tipo de JavaScript podem ser representados de maneiras diferentes nas entranhas do mecanismo.
- A V8 está tentando encontrar a melhor maneira de representar cada propriedade do objeto usado nos programas JS.
- Em certas situações, a V8 executa operações para marcar as formas de objetos como obsoletas e executa a migração de formas. Incluindo - implementa transições associadas à proibição de expansão de objetos.
Com base no exposto, podemos fornecer algumas dicas práticas de programação JavaScript que podem ajudar a melhorar o desempenho do código:
- Sempre inicialize seus objetos da mesma maneira. Isso contribui para o trabalho eficaz com formas de objetos.
- Selecione com responsabilidade os valores iniciais para os campos dos objetos. Isso ajudará os mecanismos JavaScript a escolher como representar internamente esses valores.
Caros leitores! Você já otimizou seu código com base nos recursos internos de determinados mecanismos JavaScript?
