V8 Engine: How does it work?

V8 is a javascript engine created at Google's development center in Germany. The engine is open source and written in C++. It is used both on the client side (Google Chrome) and on the server side (node.js), in the ecosystem of JavaScript applications.
athletes who run: V8 by js republic
V8 was first conceptualized to increase JavaScript runtime performance in browsers. To gain speed, V8 translates JavaScript code into high-performance machine language instead of using an interpreter. The engine compiles Javascript code into assembly at runtime by implementing a JIT (Just-In-Time) compiler like most modern Javascript engines (like SpiderMonkey or Rhino (Mozilla)) do. The main difference with V8 is that it does not produce bytecode/intermediate code.
The purpose of this article is to show you and make you understand how the V8 engine works in order to produce optimized code for the client side and the server side of your applications. If you ever ask yourself the question “Should I be concerned about the performance of JavaScript”, then I answer you with a quote from Daniel Clifford (tech lead and manager of the V8 team):
In French:

“It's not just to make your current app faster, it's to make things possible that weren't possible before”

In English:

“It's not just about making your current application run faster, it's about enabling things that you have never been able to do in the past”.

Hidden class

JavaScript is prototype-based: There are no classes and objects are created using a cloning process. JS is also a dynamically typed language: types and their information are not self-explanatory, and properties can be added and even removed from objects on the fly. Efficiently accessing types and properties is a first big challenge for V8. Instead of using a dictionary-like structure to hold object properties, and do a dynamic lookup to find the location of a property (like most JavaScript engines do), V8 creates hidden classes, in runtime, so as to have an internal representation of the type system and improve property access times.
Take as an example a function Point and the creation of two objects Point:

If the layouts are the same, which they are, p et q are part of the same hidden class created by V8. This demonstrates another advantage of using hidden classes: They allow V8 to group objects that have the same properties. Right here; p et q use the same optimized code.
Now assume we want to add a property z to our object q, right after its declaration (which is absolutely normal with a dynamically typed language).
How does V8 handle this scenario? In reality, V8 creates a new hidden class each time the constructor function declares a property and it checks for changes in the hidden class. Why ? Because if two objects are created (p et q) and if a member is added to the second object (q) after its creation, V8 needs to maintain the last created hidden class (for the first object p) and the engine needs to create a new one with (for the second object q) the new member.
Example of how V8 creates hidden classes for the constructor functions
Each time a new hidden class is created, the previous one is updated with a class transition that indicates which hidden class to use instead of the previous one.

Code optimization

As V8 creates a new hidden class for each property, their creations should be minimized. For that, we must try not to add properties after the creation of the object and always initialize the members of the object in the same order (to avoid different hidden class trees).
Another tip: monomorphic operations are those that work on objects that share the same hidden class. V8 creates a hidden class when calling a function. If we call the same function but with parameters of different types, V8 needs to create another hidden class: Prefer monomorphic code to polymorphic code.

More examples of how V8 optimizes JavaScript code

Tagged values

To have an efficient representation of JavaScript numbers and objects, V8 represents them with a value of 32 bit. It uses a bit to know if it is an object (flag=1) or an integer (flag=0) called from SMall Integer or SMI because of those 31 bits. So if a numeric value is larger than 31 bits, V8 will encapsulate the number, either making it a double or creating a new object to encapsulate it.
Code Optimization: Use 31-bit signed numbers as much as possible, to avoid the costly operation of encapsulating in a JavaScript object.

Posters

V8 uses two different methods to process arrays:

  • Fast Elements: Designed for boards where all the keys are very compact. They have one linear storage buffer so that access is very efficient.

  • Dictionary items: Designed for boards where elements are scattered. They are in fact hash tables, which is more expensive to access than “Fast Elements”.

Code Optimization: Make sure that V8 uses the “Fast Element” method to deal with arrays, in other words, avoid arrays with scattered elements where the keys are not ordered incrementally. Also avoid pre-allocating large arrays. It is better that they grow as they go. Finally, do not delete elements in the arrays: all the keys would then be dispersed.

How does V8 compile JavaScript code?

V8 has two compilers!

  • Un “Full” compiler which can generate good code for any JavaScript: good code but, it's not great JIT code. The purpose of this compiler is to generate code quickly. To achieve this goal, the compiler does no type analysis and knows nothing about types. Instead, the compiler uses a strategy of inline cache or “IC” to refine knowledge about types while the program is running. IC is very efficient and increases the speed (by an order x20).

  • Un optimizing compiler which produces awesome code for most of the JavaScript language. This compiler comes later and re-compiles functions that are used multiple times (hot functions). This compiler takes the types from the inline cache and decides how best to optimize the code. However, some parts of the language are not yet supported, for example try/catch blocks. (The trick for try/catch blocks is to write the “unstable” code in a function and call it in the try block).

Code Optimization: V8 also supports de-optimization: Optimization compiler makes optimistic assumptions from inline cache about different types, de-optimization happens if these assumptions are invalid. For example, if a hidden class was created that was not intended, V8 discards the optimized code and returns to the “Full” compiler to retrieve the types from the inline cache. This process is slow and should be avoided, which is possible by avoiding changing functions after they have been optimized.

Resources

Additional resources: monomorphic vs polymorphic
source: How does the V8 engine work?, Thibault Laurens
Translation by Yoan Ribeiro