El assembly de V8 se me ha aparecido en sueños.

Se me ha aparecido y me ha dicho:

“De tu stack soy el dueño”.

Y es que si hay algo que sabe hacer V8 es generar assembly eficiente para ejecutar instrucciones de bajo nivel de la manera más optimizada posible.

Pero...
¿Cómo genera las instrucciones V8?

Lo primero sería entender lo que yo llamo el punto de entrada.

El punto de entrada es el lugar en el que una expresión literal de Javascript pasa a ser evaluada por V8.

Dependiendo del entorno en el que estemos ejecutando V8 esto puede cambiar.

  • Node.js: Aunque en Node.js existen diferentes puntos de entrada , los más utilizados serían:

    • Modo REPL.
    • Lectura de un fichero.
  • V8: En la shell d8, que podéis obtener al compilar V8, los puntos de entrada serían los mismos que node.js mencionados anteriormente, REPL o por fichero en disco.

  • En blink el punto de entrada de un código javascript se encuentra en el apartado de bindings de V8, y no es más que un tipo v8::Handle que puede llegar de la propia página web abierta, de una petición XHR, etc...

Dicho esto, continuamos con la explicación de cómo genera las instrucciones V8, pero antes que nada, una curiosidad sobre el punto de entrada de Node.js

Curiosidad (o troleo) de Node.js

Abre la shell de node:

$ node

Ahora copia y pega esta expresión literal de una declaración de una función:

function nogg(){ return "Aholic"; }

Una vez hecho esto, copia y pega exáctamente esta línea, y para terminar pulsa intro:

nogg)(

Continúa con tu vida como si nada hubiera pasado.

Ya conocemos el punto de entrada

¿Y ahora qué?... Pues es en este punto es donde entra en juego mi colega el AST.

sueño con trigo quemas dado

Un árbol de sintaxis abstracta (AST) es una representación arbórea del código fuente preparado para ser compilado a código nativo.

Cada nodo del árbol se guarda de manera independiente del árbol completo, lo cual permite un manejo rápido de asigación/deasignación de todos los nodos del árbol. Esto quiere decir que, una vez creado un nodo del árbol, este puede ser compilado y eliminado del árbol independientemente de que la sintáxis literal completa del AST en cuestión haya sido completamente evaluada/compilada.

“La programación en bajo nivel es buena para el alma del programador” — John Carmack

A continuación algunos ejemplos de nodos que pueden existir en un AST, esto nos dará un contexto de cómo está compuesto internamente un AST y qué contiene cada nodo:

  • ExpressionStatement
  • IfStatement
  • ContinueStatement
  • ReturnStatement
  • DoWhileStatement
  • ForInStatement
  • DebuggerStatement
  • ...

Dicho esto, vamos a investigar el AST generado por el siguiente código:

function dev_name(){  
    return "Carlos Hernández Gómez";
}
dev_name();  

El árbol generado para ese código sería el siguiente, ojo a los bloques de comentarios:

FUNC  
. NAME ""
. INFERRED NAME ""
. DECLS /*** DeclarationContext, hablaremos de él en otro artículo, pero básicamente es un contador de referencias para un V8:Context ***/
. . FUNCTION "dev_name" = function dev_name
. EXPRESSION STATEMENT
. . ASSIGN
. . . VAR PROXY local[0] (mode = TEMPORARY) ".result"
. . . CALL
. . . . VAR PROXY (mode = VAR) "dev_name"
. RETURN
. . VAR PROXY local[0] (mode = TEMPORARY) ".result"
/* Código de la función, muy sencillo y liviano puesto que la función simplemente hace un return de un literal string */
FUNC  
. NAME "dev_name"
. INFERRED NAME ""
. RETURN
. . LITERAL "Carlos Hernández Gómez"

/*** Una vez ejecutada, la función devuelve: ***/
"Carlos Hernández Gómez"  

En el AST generado, podemos observar un primer nodo que representa la declaración de la función, y el segundo nodo contiene el AST correspondiente a la ejecución de la función, son por lo tanto nodos distintos que cuelgan del mismo árbol pero que V8 igualmente genera.

Respecto al AST hay varias cosas interesantes que saber, la primera es que una expresión literal de javascript convertida a un AST no volverá a ser generada más adelante aunque volvamos a declarar la misma expresión, puesto que V8 mantiene una cache de declaraciones para ser más rápido.

Si por alguna razón quisieramos analizar de manera más profunda el AST que genera V8, podríamos crear nuestra propia clase y hacer que extienda de AstVisitor, esta última clase es la encargada de visitar las sentencias de una expresión literal en búsqueda de declaraciones de variables, declaraciones de setencias, declaraciones de expresiones, etc...

Bueno, ya conocemos por encima lo que ocurre cuando V8 empieza a generar el código válido para ser ejecutado.

¿Cuál es el siguiente paso?

Generar las instrucciones

V8 utiliza dos compiladores distintos, el compilador base o también llamado full compiler y el compilador avanzado comúnmente llamado Crankshaft.

Full compiler

Este compilador es responsable de generar bytecode lo más rápidamente posible, no optimiza el código generado, y es el punto de entrada de cualquier código javascript.

Mención especial en este punto, el código evaluado en V8 no tiene por qué ser compilado siempre, habitualmente el código de una función no se compila hasta que se usa por primera vez, simplemente se genera el AST correspondiente, pero en el caso de jQuery (por decir una librería cualquiera), no se va a generar todo el bytecode de la misma, sino sólo de las funciones que se vayan utilizando, con esto V8 consigue que las páginas carguen más rápido, y ya sabemos cuan importante es la velocidad de carga de una página en esto de la web.

How do you comfort a JavaScript bug?.... You console it

Cada arquitectura ( x86, x64 ...) contiene su propio fichero full-codegen-arch.cc el cual construye el bytecode específico para esa arquitectura, así que cada vez que el equipo de V8 quiere añadir soporte para una arquitectura concreta, tiene que escribir una especificación de las instrucciones de esa arquitectura en ese tipo de fichero para dar soporte a esa plataforma.

Crankshaft

Es el amigo que mola. De tu grupo de colegas, Crankshaft es el tio más guay de todos, el que liga con todas y hace que tu y tus colegas os quedéis a dos velas.

En realidad Crankshaft es el nombre marketiniano del compilador optimizador de código de V8. Partimos de la base de que Crankshaft funciona gracias al código generado por el full compiler, dicho esto, se encarga de detectar funciones "importantes", o funciones que deberían ser optimizadas. Esto puede ser por ejemplo, una función que contiene un bucle con un número elevado de iteraciones, la lógica de Crankshaft detectará este bucle, y lo optimizará en tiempo de ejecución para obtener el máximo rendimiento posible del mismo.

Partiendo del código generado por el compilador base, Crankshaft entra en acción con 3 componentes:

  • Runtime profiler: Se encarga de monitorizar el sistema e identificar código necesario de ser optimizado, por ejemplo, código que se ejecuta continuamente en un intervalo determinado de tiempo.
  • Optimizing compiler: Recompila y optmiza el código que identifica el runtime profiler.
  • Deoptimization support Supongamos que el optimizador de código ha sido demasiado optimista optimizando una parte del código javascript, el deoptimizador permite rescatar el código original generado por el base compiler y evitar así un fallo en la optimización de un código.

Crankshaft no sólo se basa en estos componentes para funcionar, pero sí que son la parte más importante del mismo.

Hay un componente más que se llama Hydrogen y al cual le dedicaremos un capítulo entero en un futuro... Pero solo si os habéis quedado con ganas de más bytecode.

Podrás derrotarme a mi, ¡pero nunca vencerás a los cobra! Oink oink oink