El material, cuya primera parte de la traducción publicamos hoy, discutirá cómo el motor V8 JavaScript selecciona las mejores formas de representar varios valores JS en la memoria, y cómo esto afecta los mecanismos internos de V8 con respecto al trabajo con los llamados
formularios. objetos (forma). Todo esto nos ayudará a resolver la esencia del reciente
problema de rendimiento de React .

Tipos de datos de JavaScript
Cada valor de JavaScript puede tener solo uno de los ocho tipos de datos existentes:
Number
,
String
,
Symbol
,
BigInt
,
Boolean
,
Undefined
,
Null
y
Object
.
Tipos de datos de JavaScriptEl tipo de valor se puede determinar utilizando el operador
typeof
, pero hay una excepción importante:
typeof 42;
Como puede ver, el comando
typeof null
devuelve
'object'
, no
'null'
, a pesar de que
null
tiene su propio tipo:
Null
. Para comprender la razón de este tipo de comportamiento, tenemos en cuenta el hecho de que el conjunto de todos los tipos de JavaScript se puede dividir en dos grupos:
- Objetos (es decir, tipo
Object
). - Valores primitivos (es decir, cualquier valor no objetivo).
A la luz de este conocimiento, resulta que
null
significa "sin valor de objeto", mientras que
undefined
significa "sin valor".
Valores primitivos, objetos, nulos e indefinidosSiguiendo estas reflexiones en el espíritu de Java, Brendan Eich diseñó JavaScript para que el operador
typeof
devolviera
'object'
para los valores de esos tipos que se encuentran en la figura anterior a la derecha. Todos los valores de objeto y
null
llegan aquí. Es por eso que la expresión
typeof null === 'object'
es verdadera, aunque hay un tipo separado
Null
en la especificación del lenguaje.
La expresión typeof v === 'objeto' es verdaderaRepresentación de valores
Los motores de JavaScript deberían poder representar cualquier valor de JavaScript en la memoria. Sin embargo, es importante tener en cuenta que los tipos de valores en JavaScript están separados de cómo los representan los motores JS en la memoria.
Por ejemplo, un valor de 42 en JavaScript es de tipo
number
.
typeof 42;
Hay varias formas de representar enteros como 42 en la memoria:
Según el estándar ECMAScript, los números son valores de coma flotante de 64 bits, conocidos como números de coma flotante de doble precisión (Float64). Sin embargo, esto no significa que los motores de JavaScript siempre almacenen números en una vista Float64. ¡Eso sería muy, muy ineficiente! Los motores pueden usar otras representaciones internas de números, siempre que el comportamiento de los valores coincida exactamente con el comportamiento de los números Float64.
La mayoría de los números en aplicaciones JS reales, como resultó, son
índices de matriz ECMAScript válidos. Es decir, enteros en el rango de 0 a 2
32 -2.
array[0];
Los motores de JavaScript pueden elegir el formato óptimo para representar dichos valores en la memoria. Esto se hace para optimizar el código que funciona con elementos de matriz mediante índices. Un procesador que realiza operaciones de acceso a memoria necesita que los índices de la matriz estén disponibles como números almacenados en una vista con una
suma de dos . Si, en cambio, representamos los índices de las matrices en forma de valores Float64, esto significaría un desperdicio de recursos del sistema, ya que el motor necesitaría convertir los números Float64 a un formato con la suma de dos y viceversa cada vez que alguien acceda a un elemento de la matriz.
La representación de números de 32 bits con la adición de hasta dos es útil no solo para optimizar el trabajo con matrices. En general, se puede observar que el procesador realiza operaciones enteras mucho más rápido que las operaciones que utilizan valores de punto flotante. Es por eso que en el siguiente ejemplo, el primer ciclo sin problemas es el doble de rápido en comparación con el segundo ciclo.
for (let i = 0; i < 1000; ++i) {
Lo mismo se aplica a los cálculos que utilizan operadores matemáticos.
Por ejemplo, el desempeño del operador de tomar el resto de la división del siguiente fragmento de código depende de qué números estén involucrados en los cálculos.
const remainder = value % divisor;
Si ambos operandos están representados por enteros, entonces el procesador puede calcular el resultado de manera muy eficiente. Hay una optimización adicional en V8 para los casos en que el operando
divisor
está representado por un número que es una potencia de dos. Para los valores representados como números de coma flotante, los cálculos son mucho más complicados y toman mucho más tiempo.
Dado que las operaciones de enteros generalmente se realizan mucho más rápido que las operaciones en valores de punto flotante, podría parecer que los motores simplemente pueden almacenar todos los enteros y todos los resultados de las operaciones de enteros en un formato con una suma de dos. Desafortunadamente, este enfoque violaría la especificación ECMAScript. Como ya se mencionó, el estándar proporciona la representación de números en el formato Float64, y algunas operaciones con números enteros pueden dar lugar a la aparición de resultados en forma de números de punto flotante. Es importante que en tales situaciones, los motores JS produzcan resultados correctos.
Aunque en el ejemplo anterior todos los números en el lado izquierdo de las expresiones son enteros, todos los números en el lado derecho de las expresiones son valores de coma flotante. Es por eso que ninguna de las operaciones anteriores se puede realizar correctamente utilizando un formato de 32 bits con una adición de hasta dos. Los motores de JavaScript deben prestar especial atención para garantizar que, al realizar operaciones con enteros, obtenga los resultados de Float64 correctos (aunque capaces de parecer inusuales, como en el ejemplo anterior).
En el caso de los enteros pequeños que caen dentro del rango de la representación de 31 bits de los enteros con signo, V8 usa una representación especial llamada
Smi
. Todo lo que no sea un valor
Smi
se representa como un valor
HeapObject
, que es la dirección de alguna entidad en la memoria. Para los números que no entran en el rango
Smi
, tenemos un tipo especial de
HeapObject
: el llamado
HeapNumber
.
-Infinity
Como puede ver en el ejemplo anterior, algunos números JS se representan como
Smi
y otros como
HeapNumber
. El motor V8 está optimizado en términos de procesamiento de números
Smi
. El hecho es que los enteros pequeños son muy comunes en los programas JS reales. Cuando se trabaja con valores
Smi
, no es necesario asignar memoria para entidades individuales. Su uso, además, le permite realizar operaciones rápidas con enteros.
Comparación de Smi, HeapNumber y MutableHeapNumber
Hablemos de cómo se ve la estructura interna de estos mecanismos. Supongamos que tenemos el siguiente objeto:
const o = { x: 42,
El valor 42 de la propiedad del objeto
x
está codificado como
Smi
. Esto significa que se puede almacenar dentro del propio objeto. Para almacenar el valor 4.2, por otro lado, necesitará crear una entidad separada. En el objeto, habrá un enlace a esta entidad.
Almacenamiento de varios valores.Supongamos que estamos ejecutando el siguiente código JavaScript:
ox += 10;
En este caso, el valor de la propiedad
x
se puede actualizar en su ubicación de almacenamiento. El hecho es que el nuevo valor de
x
es 52, y este número se encuentra dentro del rango de
Smi
.
El nuevo valor de la propiedad x se almacena donde se almacenó el valor anterior.Sin embargo, el nuevo valor de
y
, 5.2, no encaja en el rango de
Smi
y, además, difiere del valor anterior de y - 4.2. Como resultado, V8 tiene que asignar memoria para la nueva entidad
HeapNumber
y referenciarla desde el objeto ya.
Nueva entidad HeapNumber para almacenar el nuevo valor yHeapNumber
entidades
HeapNumber
son inmutables. Esto le permite implementar algunas optimizaciones. Supongamos que queremos establecer la propiedad del objeto
x
valor de la propiedad
y
:
ox = oy;
Al realizar esta operación, podemos referirnos a la misma entidad
HeapNumber
y no asignar memoria adicional para almacenar el mismo valor.
Una de las desventajas de la inmunidad de las entidades HeapNuber es que la actualización frecuente de campos con valores fuera del rango
Smi
es lenta. Esto se demuestra en el siguiente ejemplo:
Al procesar la primera línea, se crea una instancia de
HeapNumber
, cuyo valor inicial es 0.1. En el cuerpo del ciclo, este valor cambia a 1.1, 2.1, 3.1, 4.1 y finalmente a 5.1. Como resultado, en el proceso de ejecución de este código,
HeapNumber
6 instancias de
HeapNumber
, cinco de las cuales estarán sujetas a operaciones de recolección de basura después de completar el ciclo.
HeapNumber EntitiesPara evitar este problema, V8 tiene optimización, que es un mecanismo para actualizar campos numéricos cuyos valores no se ajustan al rango
Smi
en los mismos lugares donde ya están almacenados. Si un campo numérico almacena valores para los cuales la entidad
Smi
no
Smi
adecuada para el almacenamiento, entonces V8, en forma de objeto, marca este campo como
Double
y asigna memoria para la entidad
MutableHeapNumber
, que almacena el valor real representado en el formato Float64.
Uso de entidades MutableHeapNumberComo resultado, después de que el valor del campo cambia, V8 ya no necesita asignar memoria para la nueva entidad
HeapNumber
. En cambio, simplemente escriba el nuevo valor en una entidad
MutableHeapNumber
existente.
Escribir un nuevo valor en MutableHeapNumberSin embargo, este enfoque tiene sus inconvenientes. Es decir, dado que los valores de
MutableHeapNumber
pueden cambiar, es importante asegurarse de que el sistema funcione de tal manera que estos valores se comporten según lo dispuesto en la especificación del lenguaje.
Desventajas de MutableHeapNumberPor ejemplo, si asigna el valor de
ox
alguna otra variable
y
, entonces es necesario que el valor de
y
no cambie con un cambio posterior en
ox
. ¡Eso sería una violación de la especificación de JavaScript! Como resultado, al acceder a
ox
, el número debe volverse a empaquetar con el valor habitual de
HeapNumber
antes de que se le asigne
y
.
En el caso de los números de coma flotante, V8 realiza las operaciones de empaquetado anteriores utilizando sus mecanismos internos. Pero en el caso de los enteros pequeños, usar
MutableHeapNumber
sería una pérdida de tiempo, porque
Smi
es una forma más eficiente de representar tales números.
const object = { x: 1 };
Para evitar el uso ineficiente de los recursos del sistema, todo lo que necesitamos hacer para trabajar con enteros pequeños es marcar los campos correspondientes en las formas de los objetos como
Smi
. Como resultado, los valores de estos campos, siempre que correspondan al rango
Smi
, se pueden actualizar directamente dentro de los objetos.
Trabaja con enteros cuyos valores caen dentro del rango de SmiContinuará ...
Estimados lectores! ¿Ha encontrado problemas de rendimiento de JavaScript causados por las características del motor JS?
