Last time, I talked about V8 and how it optimizes your JavaScript code. However, I didn’t talk about optimization tricks that are applied during compilation.

Today, I will describe some big optimizations and try to explain them. There are many compiler optimization techniques, so I will talk about the big ones only.

Hidden Classes

The bigger issue here is that JavaScript is a dynamic language. Properties of your objects can be added or deleted on the fly. It requires dynamic lookups to resolve the property’s location in the memory. This is a high-cost operation.

So what does V8 do to reduce the time required to access JavaScript properties? V8 dynamically creates the hidden class behind the scene. Let me explain what is hidden class by example.

Imagine, we have a function Point.

function Point(x, y) {
  this.x = x;
  this.y = y;
}

When Point is executed, a new object is created, called Point. V8 creates an initial hidden class for this object, called C0, for example. At this step, the object doesn’t have properties, so the hidden class is empty.

Hidden Class C0
Image Source: V8 Blog

The first statement creates new property in our object — x. It changes the Point object, so V8 creates new hidden class C1 based on C0. Also, updates C0 hidden class with transition to C1. At this step, our hidden class for Point object look like this:

Hidden Class C1
Image Source: V8 Blog

The second statement creates another property — y. The same steps as before. V8 creates new hidden class C2, based on C1 and updates C1 with the transition to C2. At this step, here is our hidden class:

Hidden Class C2
Image Source: V8 Blog

Because of the class transitions, we can reuse the hidden classes for new objects. The next time, when a new Point object is created, no new hidden classes are created. Instead, the new Point object shares the hidden class with the first Point object.

What happens when we are creating another Point object?

  • Initially, Point has no properties, so our object refers to C0
  • Adding x property, V8 follows the hidden class transition from C0 to C1 and writes the value of x at the offset specified by C1
  • Adding y property, V8 follows the hidden class transition from C1 to C2 and writes the value of y at the offset specified by C2

The concept of hidden classes allows a compiler to bypass a dictionary lookup when property is accessed. It already knows the offset of a requested property. Also, it enables V8 to use classic class-based optimization and inline caching.

Still, you need to understand that changing the order of properties in your constructor can lead to creating new hidden classes. For instance, let’s say you swapped x and y.

  • Initially, Point has no properties, so our object refers to C0
  • Afterwards, adding y property (we swap the order), V8 follows the hidden class transition from C0 to C1, but… there is no transition for y property. We have the transition from x property only
  • The same for x property

This leads to creating new hidden classes which leads to bad optimization (fix me if I’m wrong).

Inline Caching (IC)

Inline caching is an optimization technique used by some language runtimes, and first developed for Smalltalk. The goal of inline caching is to speed up runtime method binding by remembering the results of a previous method lookup directly at the call site. Inline caching is especially useful for dynamically typed languages where most if not all method binding happens at runtime and where virtual method tables often cannot be used.

The whole optimization trick is based on the caching result of the method lookup directly on the call site. Once it’s done, V8 will not process method lookup and can get the callee from the cache, invoking it without performing any more lookups.

But… this leads V8 to inject some “guard” code. Everything works great until you change the object, V8 doesn’t expect that. That’s when the “guard” code should execute. It performs a full dynamic lookup again.

Let’s try to dig in with example. We have a simple function that accepts two arguments and calls some method on one of them:

function render(shape, cursor) {
  // Some logic here...
  shape.render(cursor);
  // Some here...
}

We don’t know what the shape is, so we need to lookup for method render there. After lookup is done, we can rewrite the call as a direct call to the target method.

This works fine until you pass another instance of shape, so direct call leads us to the wrong object. “Guard” code checks these cases and if something is wrong, it returns to dynamic lookup, which returns the correct result.

Luckily, V8 can optimize these cases via Monomorphic Inline Caching (MIC) and Polymorphic Inline Caching (PIC). If you have a small amount of different shapes, that’s fine. V8 can handle this. Otherwise, it became megamorphic call site and there is no sense to optimize it.

That’s it about IC. Just another one optimization which tries to decrease amount of dynamic lookups (like hidden classes do).

On-Stack Replacement (OSR)

On-Stack Replacement is a technique for switching between different implementations of the same function.

When V8 optimizes your functions, it needs to replace the actual code with the optimized one somehow. That’s when OSR comes into play. It just takes un-optimized code of your function and replaces it with optimized one on call site.

Also, when things go wrong and optimized code can’t handle some edge case, OSR can switch back to un-optimized code and you don’t need to re-run your application.

Last steps

Those were the major optimizations that impact your program execution significantly.

Let’s finish this article with a short list of minor optimizations applied to your code (they are applied by a Hydrogen compiler, when CFG is building):

  • Inlining — is a compiler optimization that replaces a function call site with the body of the called function. This method eliminates call overhead.
  • Canonicalization — eliminates unnecessary operations and tries to simplify others.
  • Dead Code Elimination (DCE) — removes the code that does not affect the program results.
  • Dynamic Type Feedback (DTF) — as you remember, V8 collects type feedback information in your functions. This optimization extracts information from inline caches in your code and optimizes the code to handle only that one type of object.
  • … and a lot more of minor optimizations.

Eugene Obrezkov aka ghaiklor, Developer Advocate at Onix-Systems, Kirovohrad, Ukraine.

Updated:

Comments