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;
};
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):
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):
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):
All of these together in one big happy Harry Potter family tree looks like this:
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'
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.
Top comments (0)