¿Cómo funciona JavaScript: en el interior del motor V8 + 5 consejos sobre cómo escribir código optimizado

Ago 21, 2017 · 11 min leer

Hace un par de semanas empezamos una serie dirigida a cavar más profundo en JavaScript y cómo funciona realmente: pensamos que por el conocimiento de los bloques de construcción de JavaScript y cómo llegan a jugar juntos, usted será capaz de escribir mejor código y aplicaciones.,

el primer post de la serie se centró en proporcionar una visión general del motor, el tiempo de ejecución y la pila de llamadas. Esta segunda publicación se sumergirá en las partes internas del motor JavaScript V8 de Google. También proporcionaremos algunos consejos rápidos sobre cómo escribir mejor código JavaScript: las mejores prácticas que nuestro equipo de desarrollo de SessionStack sigue al crear el producto.

Overview

Un motor JavaScript es un programa o intérprete que ejecuta código JavaScript., Un motor JavaScript puede ser implementado como un intérprete estándar, o compilador just-in-time que compila JavaScript a bytecode de alguna forma.,/li>

  • SpiderMonkey — el primer motor JavaScript, que en los días impulsó Netscape Navigator, y hoy alimenta Firefox
  • JavaScriptCore — código abierto, comercializado como Nitro y desarrollado por Apple para Safari
  • KJS — motor de KDE desarrollado originalmente por Harri Porten para el navegador web Konqueror del proyecto KDE
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Edge
  • Nashorn, open source como parte de OpenJDK, escrito por Oracle Java languages and Tool Group
  • jerrycript — es un motor ligero para el Internet de las cosas.,
  • ¿por qué se creó el motor V8?

    El motor V8 creado por Google es de código abierto y está escrito en C++. Este motor se utiliza dentro de Google Chrome. A diferencia del resto de los motores, sin embargo, V8 también se utiliza para el popular nodo.tiempo de ejecución de js.

    V8 fue diseñado para aumentar el rendimiento de la ejecución de JavaScript dentro de los navegadores web., Con el fin de obtener velocidad, V8 traduce código JavaScript en código máquina más eficiente en lugar de utilizar un intérprete. Compila código JavaScript en código máquina en la ejecución mediante la implementación de un compilador JIT (Just-In-Time) como lo hacen muchos motores JavaScript modernos como SpiderMonkey o Rhino (Mozilla). La principal diferencia aquí es que V8 no produce bytecode o cualquier código intermedio.

    V8 solía tener dos compiladores

    Antes de la versión 5.,9 de V8 salió (lanzado a principios de este año), el motor utiliza dos compiladores:

    • full-codegen — un compilador simple y muy rápido que produce código máquina simple y relativamente lento.
    • cigüeñal – un compilador de optimización más complejo (justo a tiempo) que produjo código altamente optimizado.,l>
    • El subproceso principal hace lo que esperarías: recuperar tu código, compilarlo y luego ejecutarlo
    • También hay un subproceso separado para compilar, de modo que el subproceso principal pueda seguir ejecutándose mientras el primero optimiza el código
    • Un subproceso de perfilador que le dirá al tiempo de ejecución en qué métodos pasamos mucho tiempo para que el cigüeñal pueda optimizarlos
    • unos subprocesos para manejar barridos de recolectores de basura

    aprovecha full-CODEGEN que traduce directamente el JavaScript analizado en código máquina sin ninguna transformación., Esto le permite comenzar a ejecutar código máquina muy rápido. Tenga en cuenta que V8 no utiliza la representación de bytecode intermedio de esta manera eliminando la necesidad de un intérprete.

    Cuando su código se ha ejecutado durante algún tiempo, el subproceso del generador de perfiles ha recopilado suficientes datos para decir qué método debe optimizarse.

    a continuación, las optimizaciones del cigüeñal comienzan en otro hilo. Traduce el árbol de sintaxis abstracta de JavaScript a una representación estática de asignación única (ssa) de alto nivel llamada hidrógeno e intenta optimizar ese gráfico de hidrógeno. La mayoría de las optimizaciones se realizan en este nivel.,

    Inlining

    la primera optimización es insertar tanto código como sea posible por adelantado. Inlining es el proceso de reemplazar un sitio de llamada (la línea de código donde se llama a la función) con el cuerpo de la función llamada. Este simple paso permite que las optimizaciones siguientes sean más significativas.

    clase Ocultos

    JavaScript es un lenguaje basado en prototipos: no hay clases y los objetos se crean mediante un proceso de clonación., JavaScript también es un lenguaje de programación dinámico, lo que significa que las propiedades se pueden agregar o eliminar fácilmente de un objeto después de su instanciación.

    La mayoría de los intérpretes de JavaScript utilizan estructuras tipo diccionario (basadas en función hash) para almacenar la ubicación de los valores de propiedad de objetos en la memoria. Esta estructura hace que recuperar el valor de una propiedad en JavaScript sea más costoso computacionalmente de lo que sería en un lenguaje de programación no dinámico como Java o C#., En Java, todas las propiedades del objeto están determinadas por un diseño de objeto fijo antes de la compilación y no se pueden agregar o eliminar dinámicamente en tiempo de ejecución (bueno, C# tiene el tipo dinámico que es otro tema). Como resultado, los valores de las propiedades (o punteros a esas propiedades) se pueden almacenar como un búfer continuo en la memoria con un desplazamiento fijo entre cada uno. La longitud de un desplazamiento se puede determinar fácilmente en función del tipo de propiedad, mientras que esto no es posible en JavaScript, donde un tipo de propiedad puede cambiar durante el tiempo de ejecución.,

    dado que el uso de diccionarios para encontrar la ubicación de las propiedades de los objetos en la memoria es muy ineficiente, V8 utiliza un método diferente en su lugar: clases ocultas. Las clases ocultas funcionan de manera similar a los diseños de objetos fijos (clases) utilizados en lenguajes como Java, excepto que se crean en tiempo de ejecución. Ahora, veamos cómo se ven realmente:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);

    Una vez que ocurra la invocación de» New Point(1, 2)», V8 creará una clase oculta llamada»C0″.,

    No hay propiedades que se han definido para el Punto, sin embargo, así que «C0» está vacía.

    Una vez que la primera declaración » esto.X = x» se ejecuta (dentro de la función «Point»), V8 creará una segunda clase oculta llamada» C1 «que se basa en»C0». «C1″ describe la ubicación en la memoria (relativa al puntero del objeto)donde se puede encontrar la propiedad X., En este caso, » x «se almacena en el desplazamiento 0, Lo que significa que cuando se ve un objeto point en la memoria como un búfer continuo, el primer desplazamiento corresponderá a la propiedad»x». V8 también actualizará » C0 «con una» transición de clase «que establece que si se agrega una propiedad» x «a un objeto point, la clase oculta debería cambiar de» C0 «a»C1». La clase oculta para el objeto point de abajo es ahora «C1».,

    Cada vez que se agregue una nueva propiedad a un objeto, la vieja clase ocultos se actualiza con una ruta de transición a la nueva clase ocultos. Las transiciones de clases ocultas son importantes porque permiten que las clases ocultas se compartan entre objetos que se crean de la misma manera., Si dos objetos comparten una clase oculta y se agrega la misma propiedad a ambos, las transiciones garantizarán que ambos objetos reciban la misma clase oculta nueva y todo el código optimizado que viene con ella.

    Este proceso se repite cuando la declaración «este.y = y» se ejecuta (de nuevo, dentro de la función de punto, después de la » esto.x = x» declaración»).,

    se crea una nueva clase oculta llamada «C2», se agrega una transición de clase a «C1» indicando que si se agrega una propiedad «y» a un objeto de punto (que ya contiene la propiedad «x»), entonces la clase oculta debe cambiar a «C2», y la clase oculta del objeto de punto se actualiza a «C2».

    Oculto transiciones de clase son dependientes en el orden en que las propiedades se agrega a un objeto., Echa un vistazo al fragmento de código a continuación:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);
    p1.a = 5;
    p1.b = 6;var p2 = new Point(3, 4);
    p2.b = 7;
    p2.a = 8;

    ahora, asumirías que tanto para p1 como para p2 se usarían las mismas clases ocultas y transiciones. Bueno, en realidad no. Para «p1″, primero se agregará la propiedad» a «y luego la propiedad»b». Para «p2″, sin embargo, primero se asigna» b», seguido de»a». Por lo tanto, «p1» y «p2» terminan con diferentes clases ocultas como resultado de las diferentes rutas de Transición. En tales casos, es mucho mejor inicializar las propiedades dinámicas en el mismo orden para que las clases ocultas puedan reutilizarse.,

    almacenamiento en caché Inline

    V8 aprovecha otra técnica para optimizar lenguajes tipeados dinámicamente llamada almacenamiento en caché inline. El almacenamiento en caché en línea se basa en la observación de que las llamadas repetidas al mismo método tienden a ocurrir en el mismo tipo de objeto. Una explicación detallada del almacenamiento en caché en línea se puede encontrar aquí.

    vamos a tocar el concepto general de almacenamiento en caché en línea (en caso de que no tenga tiempo para revisar la explicación en profundidad anterior).

    Entonces, ¿cómo funciona?, V8 mantiene una caché del tipo de objetos que se pasaron como parámetro en llamadas recientes a métodos y utiliza esta información para hacer una suposición sobre el tipo de objeto que se pasará como parámetro en el futuro. Si V8 es capaz de hacer una buena suposición sobre el tipo de objeto que se pasará a un método, puede omitir el proceso de averiguar cómo acceder a las propiedades del objeto, y en su lugar, Usar la información almacenada de búsquedas anteriores a la clase oculta del objeto.

    entonces, ¿cómo se relacionan los conceptos de clases ocultas y almacenamiento en caché en línea?, Cada vez que se llama a un método en un objeto específico, el motor V8 tiene que realizar una búsqueda de la clase oculta de ese objeto para determinar el desplazamiento para acceder a una propiedad específica. Después de dos llamadas exitosas del mismo método a la misma clase oculta, V8 omite la búsqueda de clase oculta y simplemente agrega el desplazamiento de la propiedad al propio puntero del objeto. Para todas las llamadas futuras de ese método, el motor V8 asume que la clase oculta no ha cambiado, y salta directamente a la dirección de memoria de una propiedad específica utilizando los desplazamientos almacenados de búsquedas anteriores., Esto aumenta enormemente la velocidad de ejecución.

    el almacenamiento en caché en línea también es la razón por la que es tan importante que los objetos del mismo tipo compartan clases ocultas. Si crea dos objetos del mismo tipo y con diferentes clases ocultas (como hicimos en el ejemplo anterior), V8 no podrá usar el almacenamiento en caché en línea porque a pesar de que los dos objetos son del mismo tipo, sus clases ocultas correspondientes asignan diferentes compensaciones a sus propiedades.,

    Los dos objetos son básicamente las mismas, pero la «a» y «b» propiedades fueron creados en diferente orden.

    Compilación a código máquina

    Una vez que el gráfico de hidrógeno está optimizado, El cigüeñal lo baja a una representación de nivel inferior llamada litio. La mayor parte de la implementación de litio es específica de la arquitectura. La asignación de Registros ocurre en este nivel.,

    al final, el litio se compila en código máquina. Luego sucede algo más llamado OSR: reemplazo en la pila. Antes de comenzar a compilar y optimizar un método obviamente de larga duración, probablemente lo estábamos ejecutando. V8 no va a olvidar lo que acaba de ejecutar lentamente para empezar de nuevo con la versión optimizada. En su lugar, transformará todo el contexto que tengamos (pila, registros) para que podamos cambiar a la versión optimizada en medio de la ejecución. Esta es una tarea muy compleja, teniendo en cuenta que entre otras optimizaciones, V8 ha insertado el código inicialmente., V8 no es el único motor capaz de hacerlo.

    hay salvaguardas llamadas deoptimización para hacer la transformación opuesta y vuelve al código no optimizado en caso de que una suposición hecha por el motor ya no sea cierta.

    recolección de basura

    para la recolección de basura, V8 utiliza un enfoque generacional tradicional de marcar y barrer para limpiar la generación anterior. Se supone que la fase de marcado detiene la ejecución de JavaScript., Con el fin de controlar los costos de GC y hacer que la ejecución sea más estable, V8 utiliza el marcado incremental: en lugar de recorrer todo el montón, tratando de marcar todos los objetos posibles, solo recorre parte del montón, luego reanuda la ejecución normal. La siguiente parada de GC continuará desde donde se detuvo la caminata anterior. Esto permite pausas muy cortas durante la ejecución normal. Como se mencionó anteriormente, la fase de barrido se maneja por hilos separados.

    Ignition and TurboFan

    con el lanzamiento de V8 5.9 a principios de 2017, se introdujo una nueva tubería de ejecución., Esta nueva canalización logra mejoras de rendimiento aún mayores y ahorros de memoria significativos en aplicaciones JavaScript del mundo real.

    la nueva canalización de ejecución está construida sobre Ignition, el intérprete de V8, y TurboFan, el compilador de optimización más reciente de V8.

    puedes ver la publicación del blog del equipo de V8 sobre el tema aquí.

    desde la versión 5.,9 de V8 salió, full-codegen y cigüeñal (las tecnologías que han servido V8 desde 2010) ya no han sido utilizados por V8 para la ejecución de JavaScript como el equipo de V8 ha luchado para mantener el ritmo de las nuevas características del lenguaje JavaScript y las optimizaciones necesarias para estas características.

    esto significa que en general V8 tendrá una arquitectura mucho más simple y más fácil de mantener en el futuro.

    Mejoras en la Web y en el Nodo.,JS benchmarks

    estas mejoras son solo el comienzo. La nueva tubería de encendido y turbofán allana el camino para nuevas optimizaciones que aumentarán el rendimiento de JavaScript y reducirán la huella de V8 tanto en Chrome como en Node.js en los próximos años.

    finalmente, aquí hay algunos consejos y trucos sobre cómo escribir JavaScript bien optimizado y mejor., Puede derivarlos fácilmente del contenido anterior, sin embargo, aquí hay un resumen para su conveniencia:

    Cómo escribir JavaScript optimizado

    1. Orden de las propiedades del objeto: siempre cree instancias de sus propiedades del objeto en el mismo orden para que las clases ocultas y el código optimizado posteriormente se puedan compartir.
    2. propiedades dinámicas: agregar propiedades a un objeto después de la instanciación forzará un cambio de clase oculta y ralentizará cualquier método que se haya optimizado para la clase oculta anterior. En su lugar, asigne todas las propiedades de un objeto en su constructor.,
    3. Métodos: el código que ejecuta el mismo método repetidamente se ejecutará más rápido que el código que ejecuta muchos métodos diferentes solo una vez (debido al almacenamiento en caché en línea).
    4. Arrays: evite arrays dispersos donde las claves no son números incrementales. Los arrays dispersos que no tienen todos los elementos dentro de ellos son una tabla hash. Los elementos en tales arrays son más caros de acceder. También, trate de evitar la pre-asignación de grandes matrices. Es mejor crecer sobre la marcha. Finalmente, no elimine elementos en matrices. Hace que las llaves sean escasas.
    5. Valores Etiquetados: V8 representa objetos y números con 32 bits., Usa un bit para saber si es un objeto (flag = 1) o un entero (flag = 0) llamado SMI (entero pequeño) debido a sus 31 bits. Entonces, si un valor numérico es mayor que 31 bits, V8 encajonará el número, convirtiéndolo en un doble y creando un nuevo objeto para poner el número dentro. Trate de usar números firmados de 31 bits siempre que sea posible para evitar la costosa operación de boxeo en un objeto JS.

    en SessionStack tratamos de seguir estas mejores prácticas al escribir código JavaScript altamente optimizado., La razón es que una vez que integra SessionStack en su aplicación web de producción, comienza a grabar todo: todos los cambios de DOM, interacciones de usuario, excepciones de JavaScript, rastros de pila, solicitudes de red fallidas y mensajes de depuración. con SessionStack, puede reproducir problemas en sus aplicaciones web como videos y ver todo lo que le sucedió a su usuario. Y todo esto tiene que suceder sin impacto en el rendimiento de su aplicación web.
    hay un plan gratuito que le permite comenzar de forma gratuita.

    Share

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *