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

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 parte

Obsolescê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 }; //  `x`       `Smi` bx = 0.2; //  `bx`     `Double` y = ax; 

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 objetos

Quando 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 objeto

No 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 lixo

As 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 alterado

Aqui, 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; // TypeError: Cannot add property y; //      object is not extensible 

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; // TypeError: Cannot add property y; //      object is not extensible delete object.x; // TypeError: Cannot delete property x 

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; // TypeError: Cannot add property y; //      object is not extensible delete object.x; // TypeError: Cannot delete property x object.x = 3; // TypeError: Cannot assign to read-only property x 

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 objetos

Para 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 sistema

Existem 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á confuso

V8 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 sistema

Aqui 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.actualStartTime

Observe 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.actualStartTime

Depois 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() {    //     `Double`   .    this.actualStartTime = Number.NaN;    //       ,  :    this.actualStartTime = 0;    Object.preventExtensions(this);  } } const node1 = new FiberNode(); const node2 = new FiberNode(); 

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:

 //   ! class Point {  x = null;  y = null; } const p = new Point(); px = 0.1; py = 402; 

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?

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


All Articles