Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

Fundamentos de JavaScript: por qué deberías saber cómo funciona el motor


Foto de Lee Attwood en Unsplash

Nota: este artículo es una traducción del original en inglés de Rainer Hahnekamp con su permiso expreso. Rainer es un ingeniero de software austríaco con amplia experiencia y que trabaja como freelance en el área de Viena. Puedes seguirlo en Twitter.

En este artículo quiero explicar lo que un desarrollador de software que utiliza JavaScript debe saber sobre los motores de este lenguaje para que el código escrito se ejecute correctamente de manera apropiada.

Más abajo verás una función con una sola línea de código que lo que hace es devolver la propiedad lastname del objeto que se le pase como argumento. Veremos que, con sólo añadir una propiedad a cada objeto, ¡acabamos con una caída del rendimiento de más del 700%!

Como explicaré en detalle, la falta de tipos estáticos en JavaScript es lo que provoca este comportamiento. Lo que antes se veía como una ventaja sobre otros lenguajes como C# o Java, ha resultado ser más bien una especie de "pacto con el diablo".

Pisando el freno a máxima velocidad

Por lo general, no necesitamos conocer el funcionamiento interno del motor que ejecuta nuestro código. Los fabricantes de navegadores invierten mucho en hacer que los motores ejecuten el código muy rápido.

¡Genial!

Dejemos que los demás hagan el trabajo pesado. ¿Por qué preocuparse por el funcionamiento de los motores?

En el ejemplo de código a continuación tenemos cinco objetos que almacenan los nombres y apellidos de los personajes de Star Wars. La función getName devuelve el valor de la propiedad lastname del objeto que se le pase. Medimos el tiempo total que tarda esta función en ejecutarse mil millones de veces:

(()  =>  {
  const  han  =   {firstname:  "Han",   lastname:  "Solo"};
  const  luke  =  {firstname:  "Luke",  lastname:  "Skywalker"};
  const  leia  =  {firstname:  "Leia",  lastname:  "Organa"};
  const  obi  =   {firstname:  "Obi-Wan",  lastname:  "Kenobi"};
  const  yoda  =  {firstname:  "",  lastname:  "Yoda"};
  const  people  =  [
    han,  luke,  leia,  obi,
    yoda,  luke,  leia,  obi
  ];
  const  getName  =  (person)  =>  person.lastname;
  console.time("engine");
  for(var  i  =  0;  i  <  1000  *  1000  *  1000;  i++)  {
    getName(people[i  &  7]);
  }
  console.timeEnd("engine");
})();

En un Intel i7 4510U, el tiempo de ejecución es de aproximadamente 1.2 segundos. Por el momento, todo bien. Ahora añadimos otra propiedad a cada objeto y lo ejecutamos de nuevo:

(() => {
  const han = {
    firstname: "Han", lastname: "Solo", 
    spacecraft: "Falcon"};
  const luke = {
    firstname: "Luke", lastname: "Skywalker", 
    job: "Jedi"};
  const leia = {
    firstname: "Leia", lastname: "Organa", 
    gender: "female"};
  const obi = {
    firstname: "Obi", lastname: "Wan", 
    retired: true};
  const yoda = {lastname: "Yoda"};
  const people = [
    han, luke, leia, obi, 
    yoda, luke, leia, obi];
  const getName = (person) => person.lastname;
  console.time("engine");
  for(var i = 0; i < 1000 * 1000 * 1000; i++) {
    getName(people[i & 7]);
  }
  console.timeEnd("engine");
})();

Nuestro tiempo de ejecución es ahora de 8.5 segundos, lo que es aproximadamente 7 veces más lento que nuestra primera versión. Esto es lo que se siente al pisar los frenos yendo a toda velocidad. ¿Cómo ha podido ocurrir esto?

Es hora de echar un vistazo más de cerca al motor.

Fuerzas combinadas: Intérprete y compilador

El motor es la parte encargada de leer y ejecutar el código fuente. Cada uno de los principales proveedores de navegadores tiene su propio motor. Mozilla Firefox tiene Spidermonkey, Microsoft Edge tiene Chakra/ChakraCore y Apple Safari llama a su motor JavaScriptCore. Google Chrome utiliza V8, que también es el motor de Node.js. El lanzamiento del V8 en 2008 marcó un momento crucial en la historia de los motores. V8 reemplazó la relativamente lenta interpretación de JavaScript del navegador.

La razón detrás de esta mejora tan grande radica principalmente en la combinación de intérprete y compilador. Hoy en día, los cuatro motores utilizan esta técnica. El intérprete ejecuta el código fuente casi de inmediato. El compilador traduce el código fuente en código máquina que el sistema del usuario ejecuta de manera directa.

A medida que el compilador genera el código de la máquina, aplica optimizaciones. Tanto la compilación como la optimización dan como resultado una ejecución de código más rápida a pesar del tiempo extra necesario en la fase de compilación.

La idea principal detrás de los motores modernos es combinar lo mejor de ambos mundos:

  • Rápido inicio de la aplicación del intérprete.
  • Rápida ejecución del compilador.

Sabiduría Yoda | ALWAYS TWO THERE ARE AN INTERPRETER AND A COMPILER | image tagged in yoda wisdom | made w/ Imgflip meme maker

Un motor moderno usa un intérprete y un compilador. Fuente: imgflip.com

Lograr ambos objetivos comienza con el intérprete. En paralelo, el motor marca las partes de código ejecutadas con frecuencia como "Hot Path" (Ruta Caliente) y las pasa al compilador junto con la información contextual recopilada durante la ejecución. Este proceso permite al compilador adaptar y optimizar el código para el contexto actual.

Llamamos al comportamiento del compilador "Just in Time" o simplemente JIT. Cuando el motor funciona bien, puedes imaginar ciertos escenarios en los que JavaScript incluso supera a C++ en velocidad. No es de extrañar que la mayor parte del trabajo del motor se dedique a esa "optimización contextual".

Interacción entre el Intérprete y el Compilador

Tipos estáticos en tiempo de ejecución: Caché en línea

El almacenamiento en caché en línea, o IC (de Inline Caching en inglés), es una técnica de optimización importante dentro de los motores JavaScript. El intérprete debe realizar una búsqueda antes de poder acceder a la propiedad de un objeto. Esa propiedad puede ser parte del prototipo de un objeto, tener un método getter o incluso ser accesible a través de un proxy. La búsqueda de la propiedad es bastante gravosa en términos de velocidad de ejecución.

El motor asigna cada objeto a un "tipo" que genera en tiempo de ejecución. V8 llama a estos "tipos", que no forman parte del estándar ECMAScript, clases ocultas o formas de objeto. Para que dos objetos compartan la misma forma de objeto, ambos objetos deben tener exactamente las mismas propiedades y en el mismo orden. Por lo tanto, un objeto {firstname: "Han", lastname: "Solo"} se asignaría a una clase diferente que {lastname: "Solo", lastname: "Han"}.

Con la ayuda de las formas de los objetos, el motor conoce la ubicación en memoria de cada propiedad. El motor incluye directamente esas ubicaciones en la función que accede a la propiedad (_harcodeadas_).

Lo que hace el almacenamiento en caché en línea es eliminar las operaciones de búsqueda. No es de extrañar que esto produzca una gran mejora en el rendimiento.

Volviendo a nuestro ejemplo anterior: todos los objetos de la primera ejecución tenían tan solo dos propiedades, firstname y lastname, y definidas en el mismo orden. Supongamos que el nombre interno de esta forma de objeto fuese p1. Cuando el compilador aplica IC, asume que a la función sólo se le va a pasar la forma de objeto (o "tipo" interno) p1 y que devuelve el valor lastname de inmediato.

Compilación con caché en línea

En la segunda ejecución, sin embargo, tenemos que tratar con 5 formas de objeto diferentes. Cada objeto tenía una propiedad adicional y a yoda le faltaba el firstname, que está en blanco. ¿Qué sucede una vez que estamos tratando con múltiples formas de objetos?

Patos por todas partes o Tipos Múltiples

La programación funcional tiene el conocido concepto de duck typing (si camina como un pato, nada como un pato y suena como un pato, seguramente es un pato ), en el que la buena calidad de código exige funciones que puedan manejar múltiples tipos. En nuestro caso, siempre que el objeto pasado tenga la propiedad lastname, todo irá bien.

El almacenamiento en caché en línea elimina la costosa búsqueda de la ubicación de memoria de una propiedad. Funciona mejor cuando en cada acceso a la propiedad el objeto tiene la misma forma. Esto se denomina IC monomórfica.

Si tenemos hasta cuatro formas de objeto diferentes, estamos en un estado de IC polimórfico. Al igual que en el monomórfico, el código máquina optimizado "conoce" ya las cuatro ubicaciones. Pero tiene que comprobar a cuál de los cuatro objetos posibles pertenece el argumento que se le pasa. Esto provoca una disminución del rendimiento.

Una vez que superamos el umbral de cuatro, la cosa empeora considerablemente. Ahora estaríamos en lo que se conoce como un IC megamórfico. En este estado ya no hay caché local de las posiciones de memoria. En su lugar, se tiene que buscar desde una caché global. Esto es la causa de la bajada drástica del rendimiento que hemos mencionado anteriormente.

Polimórfico y Megamórfico en Acción

A continuación, vemos una caché en línea polimórfica con 2 formas de objeto diferentes:

Caché en línea polimórfica

Y ahora la caché en línea megamórfica de nuestro ejemplo de código, con 5 formas de objeto diferentes:

Caché en línea megamórfica

Clases ECMAScript al rescate

Bien, así que teníamos 5 formas de objetos y nos encontramos con un IC megamórfico. ¿Cómo podemos arreglar esto?

Tenemos que asegurarnos de que el motor marca los 5 objetos de la misma forma. Esto significa que los objetos que creamos deben contener todas las propiedades posibles. Podríamos usar literales de objetos, pero en mi experiencia las clases JavaScript/ECMAScript son la mejor solución.

Para propiedades que no están definidas, simplemente le pasamos un null o las dejamos fuera. El constructor se asegura de que estos campos estén inicializados con un valor:

(()  =>  {
  class  Person  {
    constructor({
      firstname  =  '',
      lastname  =  '',
      nave  =  '',
      empleo  =  '',
      genero  =  '',
      retirado  =  false
    }  =  {})  {
      Object.assign(this,  {
        firstname,
        lastname,
        nave,
        empleo,
        genero,
        jubilado
        });
    }
  }
  const  han  =  new  Person({
    firstname:  'Han',
    lastname:  'Solo',
    nave:  'Falcon'
  });
  const  luke  =  new  Person({
    firstname:  'Luke',
    lastname:  'Skywalker',
    empleo:  'Jedi'
  });
  const  leia  =  new  Person({
    firstname:  'Leia',
    lastname:  'Organa',
    genero:  'female'
  });
  const  obi  =  new  Person({
    firstname:  'Obi-Wan',
    lastname:  'Kenobi',
    jubilado:  true
  });
  const  yoda  =  new  Person({  lastname:  'Yoda'  });
  const  people  =  [
    han,
    luke,
    leia,
    obi,
    yoda,
    luke,
    leia,
    obi
  ];
  const  getName  =  person  =>  person.lastname;
  console.time('engine');
  for  (var  i  =  0;  i  <  1000  *  1000  *  1000;  i++)  {
    getName(people[i  &  7]);
  }
  console.timeEnd('engine');
})();

Cuando ejecutamos de nuevo esta función, vemos que nuestro tiempo de ejecución vuelve a ser de 1.2 segundos. ¡Misión cumplida!

En resumen

Los motores JavaScript modernos combinan los beneficios del intérprete y del compilador: rápido inicio de la aplicación y rápida ejecución del código.

El almacenamiento en caché en línea es una potente técnica de optimización. Funciona mejor cuando sólo se pasa una forma de objeto a la función optimizada.

Mi ejemplo extremo muestra los efectos de los diferentes tipos de caché en línea y las penalizaciones de rendimiento de los cachés megamórficos.

Usar clases JavaScript es una buena práctica. Los transpiladores de tipo estático, como TypeScript, hacen que sea más probable la utilización de las cachés monomórficas y por lo tanto de un mejor rendimiento.

Lecturas adicionales:

campusMVP campusMVP es la mejor forma de aprender a programar online y en español. En nuestros cursos solamente encontrarás contenidos propios de alta calidad (teoría+vídeos+prácticas) creados y tutelados por los principales expertos del sector. Nosotros vamos mucho más allá de una simple colección de vídeos colgados en Internet porque nuestro principal objetivo es que tú aprendas. Ver todos los posts de campusMVP
Archivado en: Desarrollo Web

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.