Noções básicas do mecanismo JavaScript: otimização de protótipo. Parte 2

Boa tarde amigos! Foi lançado o curso "Segurança dos sistemas de informação" . Em conexão com isso, estamos compartilhando com você a parte final do artigo "Fundamentos dos mecanismos JavaScript: otimização de protótipos", cuja primeira parte pode ser lida aqui .

Também lembramos que a publicação atual é uma continuação desses dois artigos: “Noções básicas de mecanismos JavaScript: formulários gerais e cache embutido. Parte 1 " , " Noções básicas de mecanismos JavaScript: formulários gerais e cache Inline. Parte 2 " .



Classes e programação de protótipos

Agora que sabemos como obter acesso rápido às propriedades dos objetos JavaScript, podemos dar uma olhada na estrutura mais complexa das classes JavaScript. É assim que a sintaxe da classe se parece em JavaScript:

class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } 

Embora isso pareça um conceito relativamente novo para JavaScript, é apenas "açúcar sintático" para a programação de protótipo que sempre foi usada em JavaScript:

 function Bar(x) { this.x = x; } Bar.prototype.getX = function getX() { return this.x; }; 

Aqui, atribuímos a propriedade getX ao objeto getX . Isso funcionará como qualquer outro objeto, pois os protótipos em JavaScript são os mesmos objetos. Nas linguagens de programação de protótipos, como JavaScript, os métodos são acessados ​​por meio de protótipos, enquanto os campos são armazenados em instâncias específicas.

Vamos dar uma olhada no que acontece quando criamos uma nova instância do Bar , que chamaremos de foo .

 const foo = new Bar(true); 

Uma instância criada usando esse código possui um formulário com uma única propriedade 'x' . O protótipo foo é Bar.prototype , que pertence à classe Bar .



Esse Bar.prototype tem a forma de si mesmo, contendo a única propriedade 'getX' , cujo valor é determinado pela função 'getX' , que quando chamada retorna this.x O protótipo Bar.prototype é Object.prototype , que faz parte da linguagem JavaScript. Object.prototype é a raiz da árvore do protótipo, enquanto o seu protótipo é null .



Quando você cria uma nova instância da mesma classe, as duas instâncias têm a mesma forma, como já entendemos. Ambas as instâncias Bar.prototype para o mesmo objeto Bar.prototype .

Acessar propriedades do protótipo

Bem, agora sabemos o que acontece quando definimos uma classe e criamos uma nova instância. Mas o que acontece se chamarmos o método na instância, como fizemos no exemplo a seguir?

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); // ^^^^^^^^^^ 

Você pode considerar qualquer chamada de método como duas etapas separadas:

 const x = foo.getX(); // is actually two steps: const $getX = foo.getX; const x = $getX.call(foo); 

O primeiro passo é carregar o método, que na verdade é uma propriedade do protótipo (cujo valor é uma função). O segundo passo é chamar uma função com uma instância, por exemplo, o valor this . Vamos dar uma olhada mais de perto na primeira etapa em que o método getX é getX partir da instância foo .



O mecanismo inicia uma instância de foo e percebe que o formulário foo não tem 'getX' , portanto, ele precisa passar pela cadeia de protótipos para encontrá-la. Chegamos ao Bar.prototype , olhamos o formulário do protótipo, vemos que ele tem a propriedade 'getX' com deslocamento zero. Bar.prototype o valor desse deslocamento no Bar.prototype e encontramos o JSFunction getX que estávamos procurando.

A flexibilidade do JavaScript permite que os links em cadeia do protótipo sejam alterados, por exemplo:

 const foo = new Bar(true); foo.getX(); // → true Object.setPrototypeOf(foo, null); foo.getX(); // → Uncaught TypeError: foo.getX is not a function 

Neste exemplo, chamamos
 foo.getX() 
duas vezes, mas cada vez tem significados e resultados completamente diferentes. É por isso que, apesar de os protótipos serem apenas objetos em JavaScript, acelerar o acesso às propriedades de um protótipo é uma tarefa ainda mais importante para os mecanismos JavaScript do que acelerar o próprio acesso às propriedades de objetos regulares.

Na prática diária, carregar propriedades do protótipo é uma operação bastante comum: isso acontece toda vez que você chama um método!

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); // ^^^^^^^^^^ 

Anteriormente, falamos sobre como os mecanismos otimizam o carregamento de propriedades regulares usando formulários e caches embutidos. Como otimizar o carregamento das propriedades do protótipo para objetos da mesma forma? Acima, vimos como as propriedades são carregadas.



Para fazer isso rapidamente com downloads repetidos nesse caso específico, você precisa conhecer as três coisas a seguir:

  • O formulário foo não contém 'getX' e não foi alterado. Isso significa que ninguém alterou o objeto foo adicionando ou removendo uma propriedade ou alterando um dos atributos da propriedade.
  • O protótipo foo ainda é o protótipo Bar.prototype . original. Portanto, ninguém alterou o protótipo foo usando Object.setPrototypeOf() ou atribuindo-o à propriedade _proto_ especial.
  • O formulário Bar.prototype contém 'getX' e não foi alterado. Isso significa que ninguém alterou o Bar.prototype ao adicionar ou remover uma propriedade ou alterar um dos atributos da propriedade.

No caso geral, isso significa que você precisa fazer uma verificação da própria instância e mais duas verificações para cada protótipo até o protótipo que contém a propriedade desejada. As verificações 1 + 2N, onde N é o número de protótipos usados, não parece tão ruim neste caso, uma vez que a cadeia de protótipos é relativamente rasa. No entanto, os mecanismos geralmente precisam lidar com cadeias de protótipos muito mais longas, como é o caso das classes DOM regulares. Por exemplo:

 const anchor = document.createElement('a'); // → HTMLAnchorElement const title = anchor.getAttribute('title'); 

Temos um HTMLAnchorElement e chamamos o método getAttribute() . A cadeia para este elemento simples já inclui 6 protótipos! A maioria dos métodos DOM que nos interessam não está no HTMLAnchorElement protótipo HTMLAnchorElement , mas em algum lugar da cadeia.



O método getAttribute() está em Element.prototype . Isso significa que toda vez que chamamos anchor.getAttribute() , o mecanismo JavaScript precisa:

  1. Verifique se 'getAttribute' não 'getAttribute' um objeto anchor si;
  2. Verifique se o protótipo final é HTMLAnchorElement.prototype ;
  3. Confirme a ausência de 'getAttribute' lá;
  4. Verifique se o próximo protótipo é HTMLElement.prototype ;
  5. Confirme a ausência de 'getAttribute' ;
  6. Verifique se o próximo protótipo é Element.prototype ;
  7. Verifique se 'getAttribute' presente nele.

Um total de 7 cheques. Como esse tipo de código é bastante comum na Web, os mecanismos usam vários truques para reduzir o número de verificações necessárias para carregar as propriedades do protótipo.

Voltando a um exemplo anterior, no qual fizemos apenas três verificações ao solicitar 'getX' para foo :

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX; 

Para cada objeto que ocorre antes do protótipo que contém a propriedade desejada, é necessário verificar os formulários quanto à ausência dessa propriedade. Seria bom se pudéssemos reduzir o número de verificações apresentando a verificação do protótipo como uma verificação da ausência de uma propriedade. Em essência, é exatamente isso que os mecanismos fazem com um truque simples: em vez de armazenar o link do protótipo na própria instância, os mecanismos o armazenam na forma.



Cada formulário indica um protótipo. Isso significa que toda vez que o protótipo muda, o mecanismo passa para uma nova forma. Agora, precisamos verificar apenas a forma do objeto para confirmar a ausência de determinadas propriedades, além de proteger o link do protótipo (proteja o link do protótipo).

Com essa abordagem, podemos reduzir o número de verificações necessárias de 2N + 1 para 1 + N para acelerar o acesso. Essa ainda é uma operação bastante cara, pois ainda é uma função linear do número de protótipos na cadeia. Os mecanismos usam vários truques para reduzir ainda mais o número de verificações para um determinado valor constante, especialmente no caso de carregamento sequencial das mesmas propriedades.

Células de validade

O V8 processa formulários de protótipo especificamente para esse fim. Cada protótipo tem uma forma exclusiva que não é compartilhada com outros objetos (em particular, com outros protótipos) e cada uma dessas formas de protótipo possui um ValidityCell especial associado a ele.



Este ValidityCell desativado sempre que alguém altera o protótipo associado a ele ou a qualquer outro protótipo acima dele. Vamos ver como isso funciona.
Para acelerar downloads de protótipos subsequentes, o V8 coloca o cache Inline em um local de quatro campos:



Quando o cache embutido é aquecido na primeira vez em que o código é executado, o V8 lembra o deslocamento no qual a propriedade foi encontrada no protótipo, esse protótipo (por exemplo, Bar.prototype ), o formulário da instância (no nosso caso, o formulário foo ) e também vincula o ValidityCell atual ao protótipo recebido da instância do formulário (no nosso caso, Bar.prototype é usado).

Na próxima vez que você usar o cache embutido, o mecanismo precisará verificar o formulário da instância e o ValidityCell . Se ainda for válido, o mecanismo usará diretamente o deslocamento no protótipo, ignorando as etapas extras de pesquisa.



Quando você altera o protótipo, um novo formulário é realçado e a célula ValidityCell anterior é desabilitada. Por esse motivo, o cache Inline é ignorado na próxima vez em que é iniciado, o que leva a um desempenho ruim.

Vamos voltar ao exemplo com o elemento DOM. Cada alteração no Object.prototype não apenas invalida os caches Inline para Object.prototype , mas também para qualquer protótipo na cadeia abaixo dele, incluindo EventTarget.prototype , Node.prototype , Element.prototype etc., até o próprio HTMLAnchorElement.prototype .



De fato, modificar o Object.prototype enquanto o código está sendo executado é uma terrível perda de desempenho. Não faça isso!

Vejamos um exemplo específico para entender melhor como isso funciona. Digamos que temos uma classe Bar e uma função loadX que chama um método em objetos do tipo Bar . Chamamos a função loadX várias vezes com instâncias da mesma classe.

 class Bar { /* … */ } function loadX(bar) { return bar.getX(); // IC for 'getX' on `Bar` instances. } loadX(new Bar(true)); loadX(new Bar(false)); // IC in `loadX` now links the `ValidityCell` for // `Bar.prototype`. Object.prototype.newMethod = y => y; // The `ValidityCell` in the `loadX` IC is invalid // now, because `Object.prototype` changed. 

O cache embutido no loadX agora aponta para ValidityCell for Bar.prototype . Se você modificar o Object.prototype (mutate), que é a raiz de todos os protótipos no JavaScript, o ValidityCell se tornará inválido e os caches Inline existentes não serão usados ​​na próxima vez, resultando em baixo desempenho.

Alterar Object.prototype é sempre uma má idéia, pois invalida qualquer cache Inline para protótipos carregados no momento da alteração. Aqui está um exemplo de como NÃO fazer:

 Object.prototype.foo = function() { /* … */ }; // Run critical code: someObject.foo(); // End of critical code. delete Object.prototype.foo; 

Estamos expandindo Object.prototype , que invalida todos os caches de protótipo Inline carregados pelo mecanismo neste momento. Em seguida, executaremos algum código que usa o método descrito por nós. O mecanismo precisará iniciar desde o início e configurar caches Inline para qualquer acesso à propriedade prototype. E então, finalmente, "limpe" e remova o método de protótipo que adicionamos anteriormente.

Você acha que limpar é uma boa ideia, certo? Bem, neste caso, vai piorar ainda mais a situação! A remoção das propriedades altera o Object.prototype , para que todos os caches inline sejam desabilitados novamente e o mecanismo precise iniciar o trabalho desde o início novamente.

Para resumir . Apesar de protótipos serem apenas objetos, eles são processados ​​especialmente por mecanismos JavaScript, a fim de otimizar o desempenho das pesquisas de métodos por protótipos. Deixe os protótipos em paz! Ou se você realmente precisar lidar com eles, faça-o antes de executar o código, para não invalidar todas as tentativas de otimizar seu código durante a execução!

Resumir

Aprendemos como o JavaScript armazena objetos e classes, e como formulários, caches embutidos e células de validade ajudam a otimizar as operações dos protótipos. Com base nesse conhecimento, entendemos como melhorar o desempenho de um ponto de vista prático: não toque em protótipos! (ou se você realmente precisar, faça-o antes de executar o código).

A primeira parte

Esta série de publicações foi útil para você? Escreva nos comentários.

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


All Articles