JavaScript engine basics: prototype optimization. Part 2
- Transfer
Good afternoon friends! The course "Security of information systems" has been launched, in connection with this we are sharing with you the final part of the article "Fundamentals of JavaScript engines: optimization of prototypes", the first part of which can be read here .
We also remind you that the current publication is a continuation of these two articles: “Basics of JavaScript engines: general forms and Inline caching. Part 1 " , " Basics of JavaScript engines: general forms and Inline caching. Part 2 " .
Classes and Prototype Programming
Now that we know how to get quick access to the properties of JavaScript objects, you can take a look at the more complex structure of JavaScript - classes. This is what the class syntax looks like in JavaScript:
Although this seems like a relatively new concept for JavaScript, it is just “syntactic sugar” for the prototype programming that has always been used in JavaScript:
Here we assign the property
Let's take a closer look at what happens when we create a new instance
An instance created using this code has a form with a single property
This one
When you create a new instance of the same class, both instances have the same form, as we already understood. Both instances will point to the same object
Access prototype properties
Well, now we know what happens when we define a class and create a new instance. But what happens if we call the method on the instance, as we did in the following example?
You can consider any method call as two separate steps:
The first step is to load the method, which is actually a property of the prototype (whose value is a function). The second step is a function call with an instance, for example, a value
The engine starts the instance
The flexibility of JavaScript allows prototype chain links to change, for example:
In this example, we call
In everyday practice, loading prototype properties is a fairly common operation: this happens every time you call a method!
Earlier, we talked about how engines optimize the loading of regular properties by using forms and inline caches. How can I optimize the loading of prototype properties for objects of the same shape? From above we saw how properties are loaded.
In order to do this quickly with repeated downloads in this particular case, you need to know the following three things:
In the general case, this means that you need to make one check of the instance itself and two more checks for each prototype up to the prototype that contains the desired property. 1 + 2N checks, where N is the number of prototypes used, does not sound so bad in this case, since the prototype chain is relatively shallow. However, engines often have to deal with much longer prototype chains, as is the case with regular DOM classes. For instance:
We have
The method
A total of 7 checks. Since this type of code is quite common on the web, engines use various tricks to reduce the number of checks required to load prototype properties.
Returning to an earlier example, in which we did only three checks when we requested
For each object that occurs before the prototype containing the desired property, it is necessary to check the forms for the absence of this property. It would be nice if we could reduce the number of checks by presenting the prototype check as a check for the absence of a property. In essence, this is exactly what engines do with a simple trick: instead of storing the prototype link to the instance itself, the engines store it in form.
Each form indicates a prototype. This means that every time a prototype changes
With this approach, we can reduce the number of required checks from 2N + 1 to 1 + N to speed up access. This is still a fairly expensive operation, as it is still a linear function of the number of prototypes in the chain. Engines use various tricks to further reduce the number of checks to a certain constant value, especially in the case of sequential loading of the same properties.
Validity cells
V8 processes prototype forms specifically for this purpose. Each prototype has a unique form that is not used in conjunction with other objects (in particular, with other prototypes), and each of these forms of the prototype has a special
This
To speed up subsequent downloads from prototypes, V8 places the Inline cache in a place with four fields:
When the inline cache is heated the first time the code is run, V8 remembers the offset at which the property was found in the prototype, this prototype (for example
The next time you use the inline cache, the engine needs to check the shape of the instance and
When changing the prototype, a new shape is highlighted, and the previous cell is
Let's go back to the example with the DOM element. Every change in
In fact, modifying
Let's look at a specific example to better understand how this works. Say we have a class
The inline cache in
Change
We are expanding
You think cleaning is a good idea, right? Well, in this case, it will further worsen the situation! Removing properties changes
Summarize. Despite the fact that prototypes are just objects, they are specially processed by JavaScript engines in order to optimize the performance of method searches by prototypes. Leave the prototypes alone! Or if you really need to deal with them, do it before executing the code, so you will at least not invalidate all attempts to optimize your code during its execution!
← First part
Was this series of publications useful to you? Write in the comments.
We also remind you that the current publication is a continuation of these two articles: “Basics of JavaScript engines: general forms and Inline caching. Part 1 " , " Basics of JavaScript engines: general forms and Inline caching. Part 2 " .
Classes and Prototype Programming
Now that we know how to get quick access to the properties of JavaScript objects, you can take a look at the more complex structure of JavaScript - classes. This is what the class syntax looks like in JavaScript:
class Bar {
constructor(x) {
this.x = x;
}
getX() {
return this.x;
}
}
Although this seems like a relatively new concept for JavaScript, it is just “syntactic sugar” for the prototype programming that has always been used in JavaScript:
function Bar(x) {
this.x = x;
}
Bar.prototype.getX = function getX() {
return this.x;
};
Here we assign the property
getX
to the object Bar.prototype
. This will work just like with any other object, since prototypes in JavaScript are the same objects. In prototype programming languages such as JavaScript, methods are accessed through prototypes, while fields are stored in specific instances. Let's take a closer look at what happens when we create a new instance
Bar
, which we will name foo
.const foo = new Bar(true);
An instance created using this code has a form with a single property
‘x’
. A prototype foo
is one Bar.prototype
that belongs to a class Bar
. This one
Bar.prototype
has the form of itself, containing a single property ‘getX’
, whose value is determined by the function ‘getX’
that returns when called this.x
. A prototype Bar.prototype
is one Object.prototype
that is part of the JavaScript language. Object.prototype
Is the root of the prototype tree, while its prototype matters null
. When you create a new instance of the same class, both instances have the same form, as we already understood. Both instances will point to the same object
Bar.prototype
. Access prototype properties
Well, now we know what happens when we define a class and create a new instance. But what happens if we call the method on the instance, as we did in the following example?
class Bar {
constructor(x) { this.x = x; }
getX() { return this.x; }
}
const foo = new Bar(true);
const x = foo.getX();
// ^^^^^^^^^^
You can consider any method call as two separate steps:
const x = foo.getX();
// is actually two steps:
const $getX = foo.getX;
const x = $getX.call(foo);
The first step is to load the method, which is actually a property of the prototype (whose value is a function). The second step is a function call with an instance, for example, a value
this
. Let's take a closer look at the first step where the method is loaded getX
from the instance foo
. The engine starts the instance
foo
and realizes that the form foo
has no property ‘getX’
, so it has to go through the prototype chain to find it. We get to Bar.prototype
, look at the prototype shape, see that it has a property ‘getX’
at zero offset. We look for meaning at this offset in Bar.prototype
and find the JSFunction getX
one we were looking for. The flexibility of JavaScript allows prototype chain links to change, for example:
const foo = new Bar(true);
foo.getX();
// → true
Object.setPrototypeOf(foo, null);
foo.getX();
// → Uncaught TypeError: foo.getX is not a function
In this example, we call
foo.getX()
twice, but each time it has completely different meanings and results. That is why, despite the fact that prototypes are just objects in JavaScript, speeding up access to prototype properties is an even more important task for JavaScript engines than speeding up their own access to properties on regular objects. In everyday practice, loading prototype properties is a fairly common operation: this happens every time you call a method!
class Bar {
constructor(x) { this.x = x; }
getX() { return this.x; }
}
const foo = new Bar(true);
const x = foo.getX();
// ^^^^^^^^^^
Earlier, we talked about how engines optimize the loading of regular properties by using forms and inline caches. How can I optimize the loading of prototype properties for objects of the same shape? From above we saw how properties are loaded.
In order to do this quickly with repeated downloads in this particular case, you need to know the following three things:
- The form
foo
does not contain‘getX’
and it has not changed. This means that no one has changed the foo object by adding or removing a property or changing one of the property attributes. - The prototype foo is still the original one
Bar.prototype
. So no one changed the prototypefoo
usingObject.setPrototypeOf()
or assigning it to a special_proto_
property. - The form
Bar.prototype
contains'getX'
and has not changed. This means that no one has changedBar.prototype
by adding or removing a property or changing one of the property attributes.
In the general case, this means that you need to make one check of the instance itself and two more checks for each prototype up to the prototype that contains the desired property. 1 + 2N checks, where N is the number of prototypes used, does not sound so bad in this case, since the prototype chain is relatively shallow. However, engines often have to deal with much longer prototype chains, as is the case with regular DOM classes. For instance:
const anchor = document.createElement('a');
// → HTMLAnchorElement
const title = anchor.getAttribute('title');
We have
HTMLAnchorElement
and we call the method getAttribute()
. The chain for this simple element already includes 6 prototypes! Most of the DOM methods we are interested in are not in the prototype itself HTMLAnchorElement
, but somewhere up the chain. The method
getAttribute()
is in Element.prototype
. This means that every time we call anchor.getAttribute()
, the JavaScript engine needs:- Check that
'getAttribute'
it is not ananchor
object in itself ; - Verify that the final prototype is
HTMLAnchorElement.prototype
; - Confirm the absence
'getAttribute'
there; - Verify that the next prototype is this
HTMLElement.prototype
; - Reaffirm absence
'getAttribute'
; - Check that the following prototype is
Element.prototype
; - Check what is present in it
'getAttribute'
.
A total of 7 checks. Since this type of code is quite common on the web, engines use various tricks to reduce the number of checks required to load prototype properties.
Returning to an earlier example, in which we did only three checks when we requested
'getX'
for foo
:class Bar {
constructor(x) { this.x = x; }
getX() { return this.x; }
}
const foo = new Bar(true);
const $getX = foo.getX;
For each object that occurs before the prototype containing the desired property, it is necessary to check the forms for the absence of this property. It would be nice if we could reduce the number of checks by presenting the prototype check as a check for the absence of a property. In essence, this is exactly what engines do with a simple trick: instead of storing the prototype link to the instance itself, the engines store it in form.
Each form indicates a prototype. This means that every time a prototype changes
foo
, the engine moves to a new form. Now we need to check only the shape of the object to confirm the absence of certain properties, as well as protect the prototype link (guard the prototype link).With this approach, we can reduce the number of required checks from 2N + 1 to 1 + N to speed up access. This is still a fairly expensive operation, as it is still a linear function of the number of prototypes in the chain. Engines use various tricks to further reduce the number of checks to a certain constant value, especially in the case of sequential loading of the same properties.
Validity cells
V8 processes prototype forms specifically for this purpose. Each prototype has a unique form that is not used in conjunction with other objects (in particular, with other prototypes), and each of these forms of the prototype has a special
ValidityCell
one that is associated with it. This
ValidityCell
each time it is disabled when someone changes the prototype associated with it or any other prototype above it. Let's see how it works. To speed up subsequent downloads from prototypes, V8 places the Inline cache in a place with four fields:
When the inline cache is heated the first time the code is run, V8 remembers the offset at which the property was found in the prototype, this prototype (for example
Bar.prototype
), the shape of the instance (in our case form foo
), and also binds the current one ValidityCell
to the prototype obtained from the form instance (in our case it is taken Bar.prototype
). The next time you use the inline cache, the engine needs to check the shape of the instance and
ValidityCell
. If it is still valid, the engine directly uses the offset on the prototype, skipping the extra search steps.When changing the prototype, a new shape is highlighted, and the previous cell is
ValidityCell
disabled. Because of this, the Inline cache is skipped the next time it starts, which leads to poor performance. Let's go back to the example with the DOM element. Every change in
Object.prototype
not just invalidates Inline caches Object.prototype
, but also for any of the prototype circuit under it, including EventTarget.prototype
, Node.prototype
, Element.prototype
, and so on. E. To the HTMLAnchorElement.prototype
. In fact, modifying
Object.prototype
while the code is executing is a terrible loss of performance. Do not do this! Let's look at a specific example to better understand how this works. Say we have a class
Bar
and a function loadX
that calls a method on objects of typeBar
. We call the function several times loadX
with instances of the same class.class Bar { /* … */ }
function loadX(bar) {
return bar.getX(); // IC for 'getX' on `Bar` instances.
}
loadX(new Bar(true));
loadX(new Bar(false));
// IC in `loadX` now links the `ValidityCell` for
// `Bar.prototype`.
Object.prototype.newMethod = y => y;
// The `ValidityCell` in the `loadX` IC is invalid
// now, because `Object.prototype` changed.
The inline cache in
loadX
now points ValidityCell
to Bar.prototype
. If you then mutate Object.prototype
, which is the root of all prototypes in JavaScript, ValidityCell
it becomes invalid and existing Inline caches will not be used the next time, resulting in poor performance. Change
Object.prototype
is always a bad idea, as it invalidates any Inline caches for loaded prototypes at the time of change. Here is an example of how NOT to do:Object.prototype.foo = function() { /* … */ };
// Run critical code:
someObject.foo();
// End of critical code.
delete Object.prototype.foo;
We are expanding
Object.prototype
that all Inline prototype caches loaded by the engine at this point are disabled. Then we will run some code that uses the method described by us. The engine will have to start from the very beginning and configure Inline caches for any access to the prototype property. And then, finally, “clean up” and remove the prototype method that we added earlier. You think cleaning is a good idea, right? Well, in this case, it will further worsen the situation! Removing properties changes
Object.prototype
, so all Inline caches are disabled again, and the engine has to start work from the very beginning again. Summarize. Despite the fact that prototypes are just objects, they are specially processed by JavaScript engines in order to optimize the performance of method searches by prototypes. Leave the prototypes alone! Or if you really need to deal with them, do it before executing the code, so you will at least not invalidate all attempts to optimize your code during its execution!
To summarize,
we learned how JavaScript stores objects and classes, and how forms, inline caches, and validity cells help optimize prototype operations. Based on this knowledge, we understood how to improve performance from a practical point of view: do not touch prototypes! (or if you really need it, do it before executing the code).
← First part
Was this series of publications useful to you? Write in the comments.