DEV Community

Cover image for Firing on all cylinders (Part 2): Understanding Hidden Classes to optimize your JavaScript code
about14sheep
about14sheep

Posted on • Edited on

Firing on all cylinders (Part 2): Understanding Hidden Classes to optimize your JavaScript code

In the first part of this series we went over the differences between dynamic and non-dynamic languages. We also went over the difference between how the two approach object storage lookup. We discussed the meaning of offset, the displacement integer in memory between an object and its properties. We then looked into how JavaScript interpreters combine all of that through the use of hash tables.

We left on a cliff hanger. Realizing that the use of hash tables is inefficient, we hinted at the way v8 mitigates this: Hidden Classes.

In part 2 of this series we learn what hidden classes are, how they work, and how the v8 JavaScript interpreter handles object storage look up efficiently.

Along the way I will stop at where, I think, the best understanding of the one-liners (mentioned in part 1) can come from.

Before we begin

Although the concepts mentioned here may not be required to get value from this post. If you are confused with the term offset, how hash tables work, or how JavaScript interpreters handle object storage lookup; I encourage you to go back and read part 1 of this series.

I have always felt that in order to understand a solution you must first understand the problem the solution solves.

The Rise of Hidden Classes

Hidden Classes are based on the same principles behind the fixed offset mapping in non-dynamic languages (see part 1). The difference is that they are created at runtime, but the outcome is the same. Hidden Classes allow the v8 interpreter to optimize property access time on objects. Hidden Classes are created for each and every object in your program.

We will go back to our example from part one of the series, the employee constructor function:

// Define a simple constructor function for an employee
const employee = function(salary, position) {
  this.salary = salary;
  this.position = position;
};
Enter fullscreen mode Exit fullscreen mode

When the v8 interpreter reads this code, it first creates a pointer to a location in memory where the call signature for the employee function is (this 'shell' does not include the properties as we learned in part 1). So you end up with your first hidden class (we can call this HC0):

Hidden Class 0

Now, when the interpreter reads the next line (this.salary) it creates a new hidden class for employee that includes the offset value for the property this.salary. It then updates the pointer to now point to this new hidden class. Also, it adds a transition from the first hidden class (HC0) to the new hidden class (HC1):

Hidden Class 1

Nest, just like previously, when the interpreter reads the next line (this.position) it creates a new hidden class (and updates the pointer) for employee that includes the offset value for the property this.position along with the already added offset value for the property this.salary. It then, also just like previously, adds a new transition from (HC1) to (HC2):

Hidden Class 2

All of these together in one big happy Harry Potter family tree looks like this:

Hidden Class All

In this image you can see the final state of the hidden classes and transitions that make up the employee constructor function.

What it all means

The transitions between the hidden classes are important. They allow for hidden classes to be shared among similar objects. What this means is that if two objects share a hidden class and you add a new property to both of them, transitions ensure that both of the objects will have the same hidden class.

This is important because being able to share hidden classes between object is what removes that need to have a hash map with each instance. Instead you have one hidden class, accessed by one quick lookup, shared among all objects of the employee type.

Now here's the catch...

The order in which you add dynamic properties to an object matters. Changing this order between two similar objects creates two different hidden classes, omitting the optimization we just discussed!

More on the catch

Let's look at what we just discussed in code. We will create two employee objects and dynamically add some properties to both of them, but we will do it out of order:

// Instantiate the two employees
const salesEmployee = new employee(50000, 'sales');
const ceoEmployee = new employee(1000000, 'ceo');

// add two new properties to salesEmployee
salesEmployee.payDay = 'Saturday';
salesEmployee.phoneNumber = 8675309;

// add the same two properties to ceoEmployee but in a different order
ceoEmployee.phoneNumber = 9087654;
ceoEmployee.payDay = 'Monday'
Enter fullscreen mode Exit fullscreen mode

This looks the same, after this is ran you have two employee's with the same structure, all conforming to the employee constructor function shape. Since the shape of the objects seems identical it seems logical to assume they will share the same hidden class and all the optimization that comes with it... right?

Nope, as it turns out the v8 interpreter will create two separate hidden classes. One for each, as the offset for the two dynamically added properties will be different. To better explain this i'll use a food analogy.

Same, Same; but Different

Imagine you are cooking a roast. There are many possible ways to cook a roast, however we will limit this discussion to just two. You might use a crock-pot and let it simmer all day pulling it out at the end to flash sear the edges. You might first sear the edges before you leave it to simmer. In both of these scenarios the ingredients are the same, however the technique is different. Both of them result in a delicious dinner, but both of them has their own distinct recipe.

This is how optimizing hidden classes works in v8. The order in which you dynamically add properties to an object matters. Either way, it is valid JavaScript just like both are valid roasts. Also just like the roasts; although the outcome is the same, the recipe is different. You have to memorize the two different techniques in cooking, so too does the v8 interpreter have to store (memorize) the two different objects and the offset of their property values.

TL;DR

When you use TypeScript, you are required to do this. The TypeScript compiler will throw an error if you try and add a property to an object dynamically. This is one of the many reasons we love TypeScript. You could almost say that the TL;DR for this post is "use TypeScript".

Final thoughts

With a better understanding of hidden classes and the catch with how you apply properties dynamically, I think this one-liner might make more sense:

always add dynamic properties to an instantiation of a class (object) in the same order

I had originally thought to add inline caching to this part of the series, however this post is already a long one. No worries though, we can just do a part 3!

Thank you for reading and if you have any questions don't hesitate to leave a comment.

Further reading

Blog: Hidden Classes in v8

Another Blog on Hidden Classes

The official v8 engine blog

Wikipedia article on Offset

Top comments (0)