Hoy publicamos la segunda parte de la traducción del material sobre los mecanismos internos de V8 y la investigación del problema de rendimiento de React.

→
La primera parteObsolescencia y migración de formas de objetos.
¿Qué pasa si el campo inicialmente contenía un
Smi
, y luego la situación cambió y necesitó almacenar un valor para el cual la representación
Smi
no
Smi
adecuada? Por ejemplo, como en el siguiente ejemplo, cuando dos objetos se representan usando la misma forma del objeto en el que
x
almacena inicialmente como
Smi
:
const a = { x: 1 }; const b = { x: 2 };
Al comienzo del ejemplo, tenemos dos objetos, para cuya representación usamos la misma forma del objeto en el que se
Smi
formato
Smi
para almacenar
x
.
La misma forma se utiliza para representar objetos.Cuando la propiedad
bx
cambia y debe usar el formato
Double
para representarla, V8 asigna espacio de memoria para la nueva forma del objeto, en el que
x
se le asigna la representación
Double
, y que indica una forma vacía. V8 también crea una entidad
MutableHeapNumber
, que se usa para almacenar el valor 0.2 de la propiedad
x
. Luego actualizamos el objeto
b
para que se refiera a esta nueva forma y cambiemos la ranura en el objeto para que se
MutableHeapNumber
a la entidad
MutableHeapNumber
creada
MutableHeapNumber
en el desplazamiento 0. Finalmente, marcamos la forma antigua del objeto como obsoleta y la desconectamos del árbol transiciones Esto se hace creando una nueva transición para
'x'
del formulario vacío al que acabamos de crear.
Consecuencias de asignar un nuevo valor a una propiedad de objetoEn este momento, no podemos eliminar completamente el formulario anterior, ya que todavía lo usa el objeto
a
. Además, será muy costoso omitir toda la memoria en la búsqueda de todos los objetos que se refieren al formulario anterior e inmediatamente actualizar el estado de estos objetos. En cambio, el V8 utiliza un enfoque "perezoso" aquí. Es decir, todas las operaciones de lectura o escritura de las propiedades del objeto
a
transfieren primero al uso de un nuevo formulario. La idea detrás de esta acción es hacer que la forma obsoleta del objeto sea inalcanzable. Esto hará que el recolector de basura se encargue de ello.
La memoria fuera de forma libera al recolector de basuraLas cosas son más complicadas en situaciones en las que el campo que cambia la vista no es el último en la cadena:
const o = { x: 1, y: 2, z: 3, }; oy = 0.1;
En este caso, el V8 necesita encontrar la llamada forma dividida. Esta es la última forma de la cadena, ubicada antes de la forma en que aparece la propiedad correspondiente. Aquí cambiamos
y
, es decir, necesitamos encontrar la última forma en la que no hubo
y
. En nuestro ejemplo, esta es la forma en que aparece
x
.
Busque la última forma en la que no hubo cambio de valorAquí, comenzando con este formulario, creamos una nueva cadena de transición para
y
que reproduce todas las transiciones anteriores. Solo que ahora la propiedad
'y'
se representará como
Double
. Ahora usamos esta nueva cadena de transición para
y
, marcándola como un viejo subárbol obsoleto. En el último paso, migramos la instancia del objeto
o
a un nuevo formulario, ahora usando la entidad
MutableHeapNumber
para almacenar el
MutableHeapNumber
y
. Con este enfoque, el nuevo objeto no usará fragmentos del viejo árbol de transición y, después de que todas las referencias a la forma anterior hayan desaparecido, la parte obsoleta del árbol también desaparecerá.
Extensibilidad e integridad de transición
El
Object.preventExtensions()
permite evitar por completo la adición de nuevas propiedades a un objeto. Si procesa el objeto con este comando e intenta agregarle una nueva propiedad, se generará una excepción. (Es cierto que si el código no se ejecuta en modo estricto, no se generará una excepción, sin embargo, un intento de agregar una propiedad simplemente no tendrá consecuencias). Aquí hay un ejemplo:
const object = { x: 1 }; Object.preventExtensions(object); object.y = 2;
El método
Object.seal()
actúa sobre los objetos de la misma manera que
Object.preventExtensions()
, pero también marca todas las propiedades como no configurables. Esto significa que no se pueden eliminar, ni se pueden cambiar sus propiedades con respecto a las posibilidades de enumerarlas, configurarlas o reescribirlas.
const object = { x: 1 }; Object.seal(object); object.y = 2;
El método
Object.freeze()
realiza las mismas acciones que
Object.seal()
, pero su uso, además, lleva al hecho de que los valores de las propiedades existentes no se pueden cambiar. Se marcan como propiedades en las que no se pueden escribir nuevos valores.
const object = { x: 1 }; Object.freeze(object); object.y = 2;
Considere un ejemplo específico. Tenemos dos objetos, cada uno de los cuales tiene un valor único
x
. Luego prohibimos la extensión del segundo objeto:
const a = { x: 1 }; const b = { x: 2 }; Object.preventExtensions(b);
El procesamiento de este código comienza con acciones que ya conocemos. Es decir, se realiza una transición desde la forma vacía del objeto a la nueva forma, que contiene la propiedad
'x'
(representada como una entidad
Smi
). Cuando prohibimos la expansión del objeto
b
, esto lleva a una transición especial a una nueva forma, que se marca como no expandible. Esta transición especial no conduce a la aparición de alguna nueva propiedad. Esto es, de hecho, solo un marcador.
El resultado de procesar un objeto utilizando el método Object.preventExtensions ()Tenga en cuenta que no podemos simplemente cambiar la forma existente con el valor
x
, ya que lo necesita otro objeto, a saber, el objeto
a
, que todavía es expandible.
Reaccionar problema de rendimiento
Ahora recopilemos todo lo que hablamos y usemos el conocimiento que hemos adquirido para comprender la esencia del reciente
problema de rendimiento de React. Cuando el equipo de React hizo un perfil de aplicaciones reales, notaron una extraña degradación en el rendimiento de V8 que actuó en el núcleo de React. Aquí hay una reproducción simplificada de la parte problemática del código:
const o = { x: 1, y: 2 }; Object.preventExtensions(o); oy = 0.2;
Tenemos un objeto con dos campos representados como entidades
Smi
. Prevenimos una mayor expansión del objeto y luego realizamos una acción que lleva al hecho de que el segundo campo debe estar representado en formato
Double
.
Ya hemos descubierto que la prohibición de la expansión de objetos conduce aproximadamente a la siguiente situación.
Consecuencias de la prohibición de expansión de objetos.Para representar ambas propiedades del objeto, se
Smi
entidades
Smi
, y se necesita la última transición para marcar la forma del objeto como no extensible.
Ahora necesitamos cambiar la forma en que la propiedad
y
está representada por
Double
. Esto significa que debemos comenzar a buscar una forma de separación. En este caso, esta es la forma en que aparece la propiedad
x
. Pero ahora el V8 está confundido. El hecho es que la forma de separación era extensible, y la forma actual estaba marcada como no extensible. V8 no sabe cómo reproducir el proceso de transición en una situación similar. Como resultado, el motor simplemente se niega a tratar de resolverlo todo. En cambio, simplemente crea un formulario separado que no está conectado al árbol de formulario actual y no se comparte con otros objetos. Esto es algo así como una forma huérfana de un objeto.
Forma huérfanaEs fácil adivinar que esto, si esto sucede con muchos objetos, es muy malo. El hecho es que esto hace que todo el sistema de formas de objetos V8 sea inútil.
Cuando ocurrió un problema reciente de React, sucedió lo siguiente. Cada objeto de la clase
FiberNode
tenía campos destinados a almacenar marcas de tiempo cuando la creación de perfiles está habilitada.
class FiberNode { constructor() { this.actualStartTime = 0; Object.preventExtensions(this); } } const node1 = new FiberNode(); const node2 = new FiberNode();
Estos campos (por ejemplo,
actualStartTime
) se inicializaron a 0 o -1. Esto condujo al hecho de que
Smi
entidades
Smi
se usaban para representar sus significados
Smi
. Pero más tarde, guardaron sellos en tiempo real en el formato de números de punto flotante devueltos por el método
performance.now (). Esto llevó al hecho de que estos valores ya no podían representarse en forma de
Smi
. Para representar estos campos, ahora se requerían entidades
Double
. Además de todo esto, React también evitó la expansión de instancias de la clase
FiberNode
.
Inicialmente, nuestro ejemplo simplificado podría presentarse de la siguiente forma.
Estado inicial del sistemaHay dos instancias de la clase que comparten el mismo árbol de transiciones de la forma de los objetos. Estrictamente hablando, para esto está diseñado el sistema de formas de objetos en V8. Pero luego, cuando los sellos en tiempo real se almacenan en el objeto, V8 no puede entender cómo puede encontrar la forma de separación.
V8 está confundidoV8 asigna una nueva forma huérfana al
node1
. Lo mismo sucede un poco más tarde con el objeto
node2
. Como resultado, ahora tenemos dos formas "huérfanas", cada una de las cuales es utilizada por un solo objeto. En muchas aplicaciones reales de React, el número de tales objetos es mucho más que dos. Estos pueden ser decenas o incluso miles de objetos de la clase
FiberNode
. Es fácil entender que esta situación no afecta muy bien el rendimiento del V8.
Afortunadamente,
solucionamos este problema en
V8 v7.4 , y estamos
explorando la posibilidad de hacer que la operación de cambiar la representación de campos de objetos requiera menos recursos. Esto nos permitirá resolver los problemas de rendimiento restantes que surgen en tales situaciones. V8, gracias a la solución, ahora se comporta correctamente en la situación del problema descrito anteriormente.
Estado inicial del sistemaAsí es como se ve. Dos instancias de la clase
FiberNode
referencia a un formulario no extensible. En este caso,
'actualStartTime'
se representa como un campo
Smi
. Cuando se realiza la primera operación de asignar un valor a la propiedad
node1.actualStartTime
, se crea una nueva cadena de transición y la cadena anterior se marca como obsoleta.
Resultados de asignar un nuevo valor a la propiedad Node1.actualStartTimeTenga en cuenta que la transición a la forma no expandible ahora se reproduce correctamente en la nueva cadena. Esto es en lo que se mete el sistema después de cambiar el valor de
node2.actualStartTime
.
Los resultados de asignar un nuevo valor a la propiedad node2.actualStartTimeDespués de asignar el nuevo valor a la propiedad
node2.actualStartTime
, ambos objetos hacen referencia al nuevo formulario y el recolector de basura puede destruir la parte obsoleta del árbol de transición.
Tenga en cuenta que las operaciones para marcar las formas de los objetos como obsoletas y su migración pueden parecer algo complicado. De hecho, tal como es. Sospechamos que en sitios web reales esto hace más daño (en términos de rendimiento, uso de memoria, complejidad) que bueno. Especialmente: después, en el caso de la
compresión del
puntero , ya no podemos usar este enfoque para almacenar campos
Double
en forma de valores incrustados en objetos. Como resultado, esperamos
abandonar por completo el mecanismo de obsolescencia de las formas de objetos V8 y hacer que este mecanismo sea obsoleto.
Cabe señalar que el equipo React
resolvió este problema por su cuenta, asegurándose de que los campos en los objetos de la clase
FiberNodes
inicialmente representados por valores Dobles:
class FiberNode { constructor() {
Aquí, en lugar de
Number.NaN
, se puede
Number.NaN
cualquier valor de punto flotante que no se ajuste al rango
Smi
. Entre estos valores se encuentran 0.000001,
Number.MIN_VALUE
, -0 e
Infinity
.
Vale la pena señalar que el problema descrito en React era específico de V8, y que al crear algún código, los desarrolladores no necesitan esforzarse por optimizarlo en función de una versión específica de un determinado motor de JavaScript. Sin embargo, es útil poder arreglar algo optimizando el código en el caso de que las causas de algunos errores estén enraizadas en las características del motor.
Vale la pena recordar que en las entrañas de los motores JS hay muchas cosas increíbles. El desarrollador JS puede ayudar a todos estos mecanismos, si es posible sin asignar los mismos valores de variables de diferentes tipos. Por ejemplo, no debe inicializar campos numéricos como
null
, ya que esto negará todas las ventajas de observar la presentación del campo y mejorará la legibilidad del código:
En otras palabras, ¡escriba código legible y el rendimiento vendrá solo!
Resumen
En este artículo, examinamos los siguientes temas importantes:
- JavaScript distingue entre valores "primitivos" y "objeto", y no se puede confiar en el tipo de resultados.
- Incluso los valores que tienen el mismo tipo de JavaScript se pueden representar de diferentes maneras en las entrañas del motor.
- V8 está tratando de encontrar la mejor manera de representar cada propiedad del objeto utilizado en los programas JS.
- En ciertas situaciones, V8 realiza operaciones para marcar las formas de los objetos como obsoletos y realiza la migración de formas. Incluyendo: implementa transiciones asociadas con la prohibición de la expansión de objetos.
Con base en lo anterior, podemos proporcionar algunos consejos prácticos de programación de JavaScript que pueden ayudar a mejorar el rendimiento del código:
- Siempre inicialice sus objetos de la misma manera. Esto contribuye al trabajo efectivo con formas de objetos.
- Seleccione responsablemente los valores iniciales para los campos de objetos. Esto ayudará a los motores de JavaScript a elegir cómo representar internamente estos valores.
Estimados lectores! ¿Alguna vez ha optimizado su código basado en las características internas de ciertos motores de JavaScript?
