Olá amigos. No final de abril, estamos lançando um novo curso
"Segurança de Sistemas de Informação" . E agora queremos compartilhar com você uma tradução do artigo, que certamente será muito útil para o curso. O artigo original pode ser
encontrado aqui .
O artigo descreve os principais fundamentos, eles são comuns a todos os mecanismos JavaScript, e não apenas à
V8 , na qual os autores do mecanismo (
Benedict e
Matias ) estão trabalhando. Como desenvolvedor de JavaScript, posso dizer que uma compreensão mais profunda de como funciona o mecanismo JavaScript ajudará você a descobrir como escrever código eficiente.

Nota : se você preferir assistir a apresentações do que ler artigos, assista a este vídeo . Caso contrário, pule e continue lendo.
Mecanismo JavaScript de pipeline (pipeline)Tudo começa com o fato de você escrever o código JavaScript. Depois disso, o mecanismo JavaScript processa o código-fonte e o apresenta como uma árvore de sintaxe abstrata (AST). Com base no AST construído, o intérprete pode finalmente começar a trabalhar e começar a gerar bytecode. Ótimo! Este é o momento em que o mecanismo executa o código JavaScript.

Para torná-lo mais rápido, você pode enviar o bytecode para o compilador de otimização junto com os dados de criação de perfil. O compilador de otimização faz certas suposições com base nos dados de criação de perfil e gera código de máquina altamente otimizado.
Se, em algum momento, as suposições estiverem incorretas, o compilador de otimização des otimiza o código e retorna ao estágio do intérprete.
Interpreter pipelines / compilador em mecanismos JavaScriptAgora, vamos examinar mais de perto as partes do pipeline que executam seu código JavaScript, ou seja, onde o código é interpretado e otimizado, e também algumas diferenças entre os principais mecanismos JavaScript.
No centro de tudo, há um pipeline que contém um intérprete e um compilador de otimização. O intérprete gera rapidamente código de bytes não otimizado, o compilador de otimização, por sua vez, trabalha por mais tempo, mas a saída possui código de máquina altamente otimizado.

A seguir, um pipeline que mostra como a V8 funciona, o mecanismo JavaScript usado pelo Chrome e pelo Node.js.

O intérprete na V8 é chamado Ignition, responsável por gerar e executar o bytecode. Ele coleta dados de criação de perfil que podem ser usados para acelerar a execução na próxima etapa enquanto o bytecode está sendo processado. Quando uma função fica
quente , por exemplo, se for iniciada com freqüência, os dados do bycode e de criação de perfil gerados são transferidos para o TurboFan, ou seja, para o compilador de otimização para gerar código de máquina altamente otimizado com base nos dados de criação de perfil.

Por exemplo, o mecanismo SpiderMonkey JavaScript da Mozilla, usado no Firefox e
SpiderNode , funciona de maneira um pouco diferente. Não possui um, mas dois compiladores de otimização. O intérprete é otimizado em um compilador básico (compilador de linha de base), que produz algum código otimizado. Juntamente com os dados de criação de perfil coletados durante a execução do código, o compilador IonMonkey pode gerar código altamente otimizado. Se a otimização especulativa falhar, o IonMonkey retornará ao código de linha de base.

Chakra - o mecanismo JavaScript da Microsoft, usado no Edge e no
Node-ChakraCore , possui uma estrutura muito semelhante e usa dois compiladores de otimização. O intérprete é otimizado no SimpleJIT (onde JIT significa "compilador Just-in-Time", que produz código um pouco otimizado. Juntamente com os dados de criação de perfil, o FullJIT pode criar um código ainda mais altamente otimizado.

JavaScriptCore (abreviado como JSC), o mecanismo JavaScript da Apple usado pelo Safari e React Native, geralmente possui três compiladores de otimização diferentes. O LLInt é um intérprete de baixo nível otimizado para o compilador base, que por sua vez é otimizado para o compilador DFG (Data Flow Graph) e já está otimizado para o compilador FTL (Faster Than Light).
Por que alguns mecanismos têm mais compiladores otimizadores do que outros? É tudo sobre compromissos. O intérprete pode processar o bytecode rapidamente, mas o bytecode sozinho não é particularmente eficiente. O compilador otimizador, por outro lado, trabalha um pouco mais, mas produz um código de máquina mais eficiente. Esse é um compromisso entre obter rapidamente o código (intérprete) ou esperar e executar o código com desempenho máximo (otimização do compilador). Alguns mecanismos optam por adicionar vários compiladores otimizadores com características diferentes de tempo e eficiência, o que permite fornecer o melhor controle sobre essa solução de comprometimento e entender o custo de complicações adicionais do dispositivo interno. Outra desvantagem é o uso de memória; confira este
artigo para entender melhor.
Acabamos de examinar as principais diferenças entre os pipelines do compilador interpretador e otimizador para vários mecanismos JavaScript. Apesar dessas diferenças de alto nível, todos os mecanismos JavaScript têm a mesma arquitetura: todos eles têm um analisador e algum tipo de pipeline de interpretador / compilador.
Modelo de objeto JavaScriptVamos ver o que mais os mecanismos JavaScript têm em comum e quais truques eles usam para acelerar o acesso às propriedades dos objetos JavaScript. Acontece que todos os principais mecanismos fazem isso de maneira semelhante.
A especificação ECMAScript define todos os objetos como dicionários com chaves de sequência que correspondem
aos atributos de
propriedade .

Além do próprio
[[Value]]
, a especificação define as seguintes propriedades:
[[Writable]]
determina se uma propriedade pode ser reatribuída;[[Enumerable]]
determina se a propriedade é exibida nos loops for-in;[[Configurable]]
determina se uma propriedade pode ser excluída.
A notação
[[ ]]
parece estranha, mas é assim que a especificação descreve propriedades em JavaScript. Você ainda pode obter esses atributos de propriedade para qualquer objeto e propriedade em JavaScript usando a API
Object.getOwnPropertyDescriptor
:
const object = { foo: 42 }; Object.getOwnPropertyDescriptor(object, 'foo');
Ok, então o JavaScript define objetos. E matrizes?
Você pode imaginar matrizes como objetos especiais. A única diferença é que as matrizes têm processamento de índice especial. Aqui, um índice de matriz é um termo especial na especificação ECMAScript. O JavaScript tem limites no número de elementos em uma matriz - até 2³² - 1. Um índice de matriz é qualquer índice disponível desse intervalo, ou seja, qualquer valor inteiro de 0 a 2³² - 2.
Outra diferença é que as matrizes têm a propriedade mágica de
length
.
const array = ['a', 'b']; array.length;
Neste exemplo, a matriz tem um comprimento de 2 no momento da criação. Em seguida, atribuímos outro elemento ao índice 2 e o comprimento aumenta automaticamente.
JavaScript define matrizes, bem como objetos. Por exemplo, todas as chaves, incluindo índices de matriz, são representadas explicitamente como seqüências de caracteres. O primeiro elemento da matriz é armazenado sob a tecla '0'.

A propriedade
length
é apenas outra propriedade que acaba sendo não enumerável e não configurável.
Assim que um elemento é adicionado à matriz, o JavaScript atualiza automaticamente o atributo da propriedade
[[Value]]
propriedade
length
.

Em geral, podemos dizer que matrizes se comportam de maneira semelhante aos objetos.
Otimização de acesso a propriedadesAgora que sabemos como os objetos são definidos em JavaScript, vamos dar uma olhada em como os mecanismos JavaScript permitem que você trabalhe com objetos com eficiência.
Na vida cotidiana, o acesso às propriedades é a operação mais comum. É extremamente importante que o mecanismo faça isso rapidamente.
const object = { foo: 'bar', baz: 'qux', };
FormuláriosNos programas JavaScript, é prática comum atribuir as mesmas chaves de propriedade a muitos objetos. Eles dizem que esses objetos têm a mesma
forma .
const object1 = { x: 1, y: 2 }; const object2 = { x: 3, y: 4 };
Também a mecânica comum é o acesso à propriedade de objetos da mesma forma:
function logX(object) { console.log(object.x);
Sabendo disso, os mecanismos JavaScript podem otimizar o acesso à propriedade de um objeto com base em sua forma. Veja como funciona.
Suponha que tenhamos um objeto com propriedades x e y, ele use a estrutura de dados do dicionário, sobre a qual falamos anteriormente; contém cadeias de teclas que apontam para seus respectivos atributos.

Se você acessar uma propriedade, como
object.y,
o mecanismo JavaScript procurará um JSObject com a chave
'y'
, depois carregará os atributos de propriedade que correspondem a essa consulta e finalmente retornará
[[Value]]
.
Mas onde esses atributos de propriedade são armazenados na memória? Devemos armazená-los como parte de um JSObject? Se fizermos isso, veremos mais objetos desse formulário posteriormente; nesse caso, é um desperdício de espaço armazenar um dicionário completo contendo os nomes de propriedades e atributos no próprio JSObject, já que os nomes de propriedades são repetidos para todos os objetos do mesmo formulário. Isso causa muita duplicação e leva à má alocação de memória. Para otimização, os mecanismos armazenam a forma do objeto separadamente.

Esta
Shape
contém todos os nomes e atributos de propriedades, exceto
[[Value]]
. Em vez disso, o formulário contém os valores de deslocamento dentro do JSObject, para que o mecanismo JavaScript saiba onde procurar os valores. Cada JSObject com um formulário comum indica uma instância específica do formulário. Agora, cada JSObject deve armazenar apenas valores exclusivos do objeto.

A vantagem se torna óbvia assim que temos muitos objetos. O número deles não importa, porque se eles tiverem um formulário, salvaremos as informações sobre o formulário e a propriedade apenas uma vez.
Todos os mecanismos JavaScript usam formulários como um meio de otimização, mas não os nomeiam diretamente como
shapes
:
- A documentação acadêmica os chama de classes ocultas (semelhantes às classes JavaScript);
- V8 os chama de Mapas;
- Chakra os chama de tipos;
- JavaScriptCore os chama de estruturas;
- SpiderMonkey os chama de Formas.
Neste artigo, continuamos a chamá-los de
shapes
.
Cadeias de transição e árvoresO que acontece se você tiver um objeto de uma determinada forma, mas adicionar uma nova propriedade a ele? Como o mecanismo JavaScript define um novo formulário?
const object = {}; object.x = 5; object.y = 6;
Os formulários criam o que é chamado de cadeias de transição no mecanismo JavaScript. Aqui está um exemplo:

Um objeto inicialmente não possui propriedades; corresponde a um formulário vazio. A expressão a seguir adiciona a propriedade
'x'
com o valor 5 a esse objeto e, em seguida, o mecanismo segue para o formulário que contém a propriedade
'x'
e o valor 5 é adicionado ao JSObject no primeiro deslocamento 0. A próxima linha adiciona a propriedade
'y'
e o mecanismo passa à próxima um formulário que já contém
'x'
e
'y'
e também adiciona o valor 6 ao JSObject no deslocamento 1.
Nota : A sequência na qual as propriedades são adicionadas afeta o formulário. Por exemplo, {x: 4, y: 5} resultará em uma forma diferente de {y: 5, x: 4}.
Nem precisamos armazenar a tabela de propriedades inteira para cada formulário. Em vez disso, cada formulário precisa conhecer apenas uma nova propriedade que eles estão tentando incluir nele. Por exemplo, neste caso, não precisamos armazenar informações sobre 'x' na última forma, pois elas podem ser encontradas anteriormente na cadeia. Para que isso funcione, o formulário é mesclado com o formulário anterior.

Se você escrever
ox
no seu código JavaScript, o JavaScript procurará a propriedade
'x'
ao longo da cadeia de transição até detectar um formulário que já tenha a propriedade
'x'
.
Mas o que acontece se for impossível criar uma cadeia de transição? Por exemplo, o que acontece se você tiver dois objetos vazios e adicionar propriedades diferentes a eles?
const object1 = {}; object1.x = 5; const object2 = {}; object2.y = 6;
Nesse caso, um ramo aparece e, em vez da cadeia de transição, obtemos uma árvore de transição:

Criamos um objeto vazio
a
e adicionamos a propriedade
'x'
. Como resultado, temos um
JSObject
contendo um único valor e dois formulários: vazio e um formulário com uma única propriedade
'x'
.
O segundo exemplo começa com o fato de termos um objeto vazio
b
, mas adicionamos outra propriedade
'y'
. Como resultado, aqui temos duas cadeias de formas, mas no final temos três cadeias.
Isso significa que sempre começamos com um formulário vazio? Não necessariamente. Os mecanismos usam alguma otimização de literais de objetos, que já contêm propriedades. Digamos que adicionamos x, começando com um literal de objeto vazio ou temos um literal de objeto que já contém
x
:
const object1 = {}; object1.x = 5; const object2 = { x: 6 };
No primeiro exemplo, começamos com um formulário vazio e vamos para uma cadeia que também contém
x
, como vimos anteriormente.
No caso do
object2
faz sentido criar diretamente objetos que já possuem x desde o início, em vez de começar com um objeto vazio e uma transição.

O literal de um objeto que contém a propriedade
'x'
começa com um formulário que contém
'x'
desde o início e o formulário vazio é efetivamente ignorado. Isto é (pelo menos) o que V8 e SpiderMonkey fazem. A otimização reduz a cadeia de transição e facilita a montagem de objetos a partir de literais.
A publicação no blog de Benedict sobre o incrível polimorfismo de aplicativos no
React fala sobre como essas sutilezas podem afetar o desempenho.
Além disso, você verá um exemplo de pontos de um objeto tridimensional com as propriedades
'x'
,
'y'
,
'z'
.
const point = {}; point.x = 4; point.y = 5; point.z = 6;
Como você entendeu anteriormente, criamos um objeto com três formas na memória (sem contar a forma vazia). Para acessar a propriedade
'x'
desse objeto, por exemplo, se você escrever
point.x
em seu programa, o mecanismo JavaScript deverá seguir uma lista vinculada: iniciando no formulário na parte inferior e, em seguida, movendo-se gradualmente para o formulário que possui
'x'
no topo.

Acontece muito lentamente, especialmente se você faz isso com frequência e com muitas propriedades do objeto. O tempo de permanência de uma propriedade é
O(n)
, ou seja, é uma função linear que se correlaciona com o número de propriedades do objeto. Para acelerar as pesquisas de propriedades, os mecanismos JavaScript adicionam uma estrutura de dados ShapeTable. ShapeTable é um dicionário onde as chaves são mapeadas de uma certa maneira com os formulários e produzem a propriedade desejada.

Espere um segundo, agora voltamos à busca no dicionário ... Foi exatamente assim que começamos quando colocamos formulários em primeiro lugar! Então, por que nos preocupamos com formulários?
O fato é que os formulários contribuem para outra otimização chamada
caches em linha.Falaremos sobre o conceito de caches em linha ou ICs na
segunda parte do artigo, mas agora queremos convidá-lo para um
seminário on-line gratuito , que será realizado pelo famoso analista viral e professor de meio período,
Alexander Kolesnikov , em 9 de abril.