Hola amigos A finales de abril, estamos lanzando un nuevo curso
"Seguridad de los sistemas de información" . Y ahora queremos compartir con ustedes una traducción del artículo, que sin duda será muy útil para el curso. El artículo original se puede
encontrar aquí .
El artículo describe los fundamentos clave, son comunes a todos los motores de JavaScript, y no solo a
V8 , en el que están trabajando los autores del motor (
Benedict y
Matias ). Como desarrollador de JavaScript, puedo decir que una comprensión más profunda de cómo funciona el motor de JavaScript lo ayudará a descubrir cómo escribir código eficiente.

Nota : si prefiere ver presentaciones que leer artículos, mire este video . De lo contrario, sáltelo y siga leyendo.
Motor JavaScript de canalización (canalización)Todo comienza con el hecho de que escribes código JavaScript. Después de eso, el motor de JavaScript procesa el código fuente y lo presenta como un árbol de sintaxis abstracta (AST). Basado en el AST construido, el intérprete finalmente puede ponerse a trabajar y comenzar a generar bytecode. Genial Este es el momento en que el motor ejecuta el código JavaScript.

Para que funcione más rápido, puede enviar bytecode al compilador de optimización junto con los datos de creación de perfiles. El compilador de optimización realiza ciertas suposiciones basadas en datos de perfil, luego genera un código de máquina altamente optimizado.
Si en algún momento las suposiciones resultan ser incorrectas, el compilador de optimización des-optimizará el código y volverá a la etapa de intérprete.
Tuberías de intérprete / compilador en motores JavaScriptAhora echemos un vistazo más de cerca a las partes de la tubería que ejecutan su código JavaScript, es decir, dónde se interpreta y optimiza el código, y también veamos algunas diferencias entre los principales motores JavaScript.
En el corazón de todo hay una tubería que contiene un intérprete y un compilador optimizador. El intérprete genera rápidamente un código de bytes no optimizado, el compilador de optimización, a su vez, trabaja más tiempo, pero la salida tiene un código de máquina altamente optimizado.

El siguiente es un canal que muestra cómo funciona V8, el motor de JavaScript utilizado por Chrome y Node.js.

El intérprete en V8 se llama Ignition, que es responsable de generar y ejecutar bytecode. Recopila datos de creación de perfiles que se pueden utilizar para acelerar la ejecución en el siguiente paso mientras se procesa el código de bytes. Cuando una función se
calienta , por ejemplo, si se inicia con frecuencia, el bytecode generado y los datos de perfil se transfieren al TurboFan, es decir, al compilador de optimización para generar código de máquina altamente optimizado basado en los datos de perfil.

Por ejemplo, el motor JavaScript SpiderMonkey de Mozilla, que se usa en Firefox y
SpiderNode , funciona de manera un poco diferente. No tiene uno, sino dos compiladores optimizadores. El intérprete está optimizado en un compilador básico (compilador de línea de base), que produce un código optimizado. Junto con los datos de perfil recopilados durante la ejecución del código, el compilador IonMonkey puede generar código altamente optimizado. Si la optimización especulativa falla, IonMonkey vuelve al código de línea de base.

Chakra: el motor JavaScript de Microsoft, utilizado en Edge y
Node-ChakraCore , tiene una estructura muy similar y utiliza dos compiladores optimizadores. El intérprete está optimizado en SimpleJIT (donde JIT significa "compilador Just-In-Time", que produce código algo optimizado. Junto con los datos de creación de perfiles, FullJIT puede crear código aún más optimizado.

JavaScriptCore (abreviado como JSC), el motor JavaScript de Apple utilizado por Safari y React Native, generalmente tiene tres compiladores de optimización diferentes. LLInt es un intérprete de bajo nivel que está optimizado para el compilador base, que a su vez está optimizado para el compilador DFG (Data Flow Graph), y ya está optimizado para el compilador FTL (Faster Than Light).
¿Por qué algunos motores tienen más compiladores optimizadores que otros? Se trata de compromisos. El intérprete puede procesar bytecode rápidamente, pero bytecode por sí solo no es particularmente eficiente. El compilador de optimización, por otro lado, funciona un poco más, pero produce un código de máquina más eficiente. Esto es un compromiso entre obtener rápidamente el código (intérprete) o esperar y ejecutar el código con el máximo rendimiento (compilador de optimización). Algunos motores eligen agregar varios compiladores de optimización con diferentes características de tiempo y eficiencia, lo que le permite proporcionar el mejor control sobre esta solución de compromiso y comprender el costo de las complicaciones adicionales del dispositivo interno. Otra compensación es el uso de memoria; consulte este
artículo para una mejor comprensión.
Acabamos de examinar las principales diferencias entre las tuberías del compilador del intérprete y el optimizador para varios motores JavaScript. A pesar de estas diferencias de alto nivel, todos los motores de JavaScript tienen la misma arquitectura: todos tienen un analizador y algún tipo de canalización de intérprete / compilador.
Modelo de objeto de JavaScriptVeamos qué más tienen en común los motores de JavaScript y qué trucos usan para acelerar el acceso a las propiedades de los objetos de JavaScript. Resulta que todos los motores principales hacen esto de manera similar.
La especificación ECMAScript define todos los objetos como diccionarios con claves de cadena que coinciden con
los atributos de
propiedad .

Además del propio
[[Value]]
, la especificación define las siguientes propiedades:
[[Writable]]
determina si una propiedad puede ser reasignada;[[Enumerable]]
determina si la propiedad se muestra en bucles for-in;[[Configurable]]
determina si una propiedad se puede eliminar.
La notación
[[ ]]
parece extraña, pero así es como la especificación describe las propiedades en JavaScript. Todavía puede obtener estos atributos de propiedad para cualquier objeto y propiedad en JavaScript utilizando la API
Object.getOwnPropertyDescriptor
:
const object = { foo: 42 }; Object.getOwnPropertyDescriptor(object, 'foo');
Ok, entonces JavaScript define objetos. ¿Qué pasa con las matrices?
Puedes imaginar las matrices como objetos especiales. La única diferencia es que las matrices tienen un procesamiento de índice especial. Aquí, un índice de matriz es un término especial en la especificación ECMAScript. JavaScript tiene límites en el número de elementos en una matriz, hasta 2³² - 1. Un índice de matriz es cualquier índice disponible de este rango, es decir, cualquier valor entero de 0 a 2³² - 2.
Otra diferencia es que las matrices tienen la propiedad mágica de la
length
.
const array = ['a', 'b']; array.length;
En este ejemplo, la matriz tiene una longitud de 2 en el momento de la creación. Luego asignamos otro elemento al índice 2 y la longitud aumenta automáticamente.
JavaScript define matrices y objetos. Por ejemplo, todas las claves, incluidos los índices de matriz, se representan explícitamente como cadenas. El primer elemento de la matriz se almacena bajo la clave '0'.

La propiedad de
length
es solo otra propiedad que resulta no enumerable ni configurable.
Tan pronto como se agrega un elemento a la matriz, JavaScript actualiza automáticamente el atributo de la propiedad
[[Value]]
propiedad de
length
.

En general, podemos decir que las matrices se comportan de manera similar a los objetos.
Optimización del acceso a las propiedades.Ahora que sabemos cómo se definen los objetos en JavaScript, echemos un vistazo a cómo los motores de JavaScript le permiten trabajar con objetos de manera eficiente.
En la vida cotidiana, el acceso a las propiedades es la operación más común. Es extremadamente importante que el motor haga esto rápidamente.
const object = { foo: 'bar', baz: 'qux', };
FormasEn los programas de JavaScript, es una práctica bastante común asignar las mismas claves de propiedad a muchos objetos. Dicen que tales objetos tienen la misma
forma .
const object1 = { x: 1, y: 2 }; const object2 = { x: 3, y: 4 };
También la mecánica común es el acceso a la propiedad de objetos de la misma forma:
function logX(object) { console.log(object.x);
Sabiendo esto, los motores de JavaScript pueden optimizar el acceso a la propiedad de un objeto en función de su forma. Mira cómo funciona.
Supongamos que tenemos un objeto con propiedades x e y, utiliza la estructura de datos del diccionario, de la que hablamos anteriormente; Contiene cadenas clave que apuntan a sus respectivos atributos.

Si accede a una propiedad, como
object.y,
el motor de JavaScript busca un JSObject con la clave
'y'
, luego carga los atributos de propiedad que coinciden con esta consulta y finalmente devuelve
[[Value]]
.
Pero, ¿dónde se almacenan estos atributos de propiedad en la memoria? ¿Deberíamos almacenarlos como parte de un JSObject? Si hacemos esto, veremos más objetos de esta forma más adelante, en cuyo caso, es un desperdicio de espacio almacenar un diccionario completo que contenga los nombres de propiedades y atributos en el propio JSObject, ya que los nombres de propiedades se repiten para todos los objetos de la misma forma. Esto causa mucha duplicación y conduce a una mala asignación de memoria. Para la optimización, los motores almacenan la forma del objeto por separado.

Esta
Shape
contiene todos los nombres de propiedades y atributos excepto
[[Value]]
. En cambio, el formulario contiene los valores de desplazamiento dentro del JSObject, por lo que el motor de JavaScript sabe dónde buscar los valores. Cada objeto JSO con un formulario común indica una instancia específica del formulario. Ahora cada JSObject tiene que almacenar solo valores que sean únicos para el objeto.

La ventaja se hace evidente en cuanto tenemos muchos objetos. Su número no importa, porque si tienen un formulario, guardamos información sobre el formulario y la propiedad solo una vez.
Todos los motores de JavaScript usan formularios como un medio de optimización, pero no los nombran directamente como
shapes
:
- La documentación académica los llama clases ocultas (similares a las clases de JavaScript);
- V8 los llama Mapas;
- Chakra los llama Tipos;
- JavaScriptCore los llama estructuras;
- SpiderMonkey los llama formas.
En este artículo, seguimos llamándolos
shapes
.
Cadenas de transición y árboles.¿Qué sucede si tiene un objeto de cierta forma, pero le agrega una nueva propiedad? ¿Cómo define el motor JavaScript un nuevo formulario?
const object = {}; object.x = 5; object.y = 6;
Los formularios crean lo que se llaman cadenas de transición en el motor de JavaScript. Aquí hay un ejemplo:

Un objeto inicialmente no tiene propiedades; corresponde a una forma vacía. La siguiente expresión agrega la propiedad
'x'
con valor 5 a este objeto, luego el motor pasa a la forma que contiene la propiedad
'x'
y el valor 5 se agrega a JSObject en el primer desplazamiento 0. La siguiente línea agrega la propiedad
'y'
, luego el motor pasa a la siguiente un formulario que ya contiene
'x'
e
'y'
, y también agrega el valor 6 a JSObject en el desplazamiento 1.
Nota : La secuencia en la que se agregan las propiedades afecta la forma. Por ejemplo, {x: 4, y: 5} dará como resultado una forma diferente a {y: 5, x: 4}.
Ni siquiera necesitamos almacenar la tabla de propiedades completa para cada formulario. En cambio, cada formulario necesita conocer solo una nueva propiedad que están tratando de incluir en ella. Por ejemplo, en este caso, no necesitamos almacenar información sobre 'x' en la última forma, ya que se puede encontrar antes en la cadena. Para que esto funcione, el formulario se combina con su formulario anterior.

Si escribe
ox
en su código JavaScript, JavaScript buscará la propiedad
'x'
largo de la cadena de transición hasta que detecte un formulario que ya tenga la propiedad
'x'
.
Pero, ¿qué sucede si es imposible crear una cadena de transición? Por ejemplo, ¿qué sucede si tiene dos objetos vacíos y les agrega diferentes propiedades?
const object1 = {}; object1.x = 5; const object2 = {}; object2.y = 6;
En este caso, aparece una rama y, en lugar de la cadena de transición, obtenemos un árbol de transición:

Creamos un objeto vacío
a
y le agregamos la propiedad
'x'
. Como resultado, tenemos un
JSObject
contiene un solo valor y dos formularios: vacío y un formulario con una sola propiedad
'x'
.
El segundo ejemplo comienza con el hecho de que tenemos un objeto vacío
b
, pero luego agregamos otra propiedad
'y'
. Como resultado, aquí obtenemos dos cadenas de formas, pero al final obtenemos tres cadenas.
¿Significa esto que siempre comenzamos con un formulario vacío? No necesariamente Los motores utilizan alguna optimización de literales de objetos, que ya contienen propiedades. Digamos que agregamos x, comenzando con un literal de objeto vacío, o tenemos un literal de objeto que ya contiene
x
:
const object1 = {}; object1.x = 5; const object2 = { x: 6 };
En el primer ejemplo, comenzamos con un formulario vacío y vamos a una cadena que también contiene
x
, tal como vimos anteriormente.
En el caso de
object2
tiene sentido crear directamente objetos que ya tengan x desde el principio, en lugar de comenzar con un objeto vacío y una transición.

El literal de un objeto que contiene la propiedad
'x'
comienza con un formulario que contiene
'x'
desde el principio, y el formulario vacío se omite efectivamente. Esto es (al menos) lo que hacen V8 y SpiderMonkey. La optimización acorta la cadena de transición y hace que sea más conveniente ensamblar objetos a partir de literales.
La publicación del blog de Benedict sobre el sorprendente polimorfismo de las aplicaciones en
React habla sobre cómo tales sutilezas pueden afectar el rendimiento.
Además, verá un ejemplo de puntos de un objeto tridimensional con las propiedades
'x'
,
'y'
,
'z'
.
const point = {}; point.x = 4; point.y = 5; point.z = 6;
Como entendió anteriormente, creamos un objeto con tres formas en la memoria (sin contar la forma vacía). Para acceder a la propiedad
'x'
de este objeto, por ejemplo, si escribe
point.x
en su programa, el motor de JavaScript debe seguir una lista vinculada: comenzando desde el formulario en la parte inferior y luego gradualmente avanzando hacia el formulario que tiene
'x'
en lo más alto.

Resulta muy lento, especialmente si lo haces con frecuencia y con muchas propiedades del objeto. El tiempo de residencia de una propiedad es
O(n)
, es decir, es una función lineal que se correlaciona con el número de propiedades del objeto. Para acelerar las búsquedas de propiedades, los motores de JavaScript agregan una estructura de datos ShapeTable. ShapeTable es un diccionario donde las claves se asignan de cierta manera con los formularios y producen la propiedad deseada.

Espera un segundo, ahora volvemos a la búsqueda del diccionario ... ¡Esto es exactamente con lo que comenzamos cuando ponemos formularios en primer lugar! Entonces, ¿por qué nos importan los formularios?
El hecho es que los formularios contribuyen a otra optimización llamada
cachés en línea.Hablaremos sobre el concepto de cachés en línea o circuitos integrados en la
segunda parte del artículo, y ahora queremos invitarlo a un
seminario web abierto gratuito , que será realizado por el famoso analista de virus y maestro a tiempo parcial,
Alexander Kolesnikov , el 9 de abril.