JS engine is a program that converts JS code into lower level or machine code that microprocessors can understand. The goal of JS engine is to generate the most optimzied code in shortest time possible.
One interesting feature of Javascript is that JS is dynamic typing. It means that we don't have to specify the type of variable when we declare it.
let number = 17;
We didn't declared the variable number
as an integer or number type. But JS engine dynamically converts it as a number as a machine code. So how does JS engine work?
First, JS engine parses the source code, and generates the Abstract Syntax Tree(AST).
Then, based on AST, interpreter generates the bytecode and executes it.
While it executes the bytecode, if the function is 'hot', which means that it is used multiple times, it sends the profiling data to the optimizing compiler.
Based on the data from previous executions, optimizing compiler generates the optimized code which is executed faster than bytecode.
If the assumption is changed, it de-optimizes and goes back to interpreter.
Often we call the JS engine does JIT compilation. It means that JS engine generates machine code during the run time, not ahead of time(AOT). So because of this, JS engine understands even we don't specify the type of variablesor objects. As I mentioned, JS engine compiles and executes together with the help of interpreter & optimizing compiler.
On the other hand, in C++, C++ engine compiles and then executes. Which means that we should specify the type of varible, like this.
int number = 17;
In V8, interpreter is called 'ignition' and optimizing compiler is called 'turbofan'. This is an example of how V8 engine works in given source code.
let result = 0;
for (let i = 0; i < 4242424242; ++i) {
result += i;
}
console.log(result);
V8 starts to run the soruce code with ignition and starts to generate and execute the bytecode. When the engine notice its 'hot'(because the same function is repeated over time), turbofan frontend starts to generate profiling data of given function and sends it to the turbofan. Turbofan starts to generate optimized code.
There are lots of different types of JS engines according to browsers.
It's good to have numerous JS engines because these engines would compete and eventually become better as time goes.But why are engines different each other? Because there isn't a sole best solution. As I said earlier, the ultimate goal of JS engine is to generate the most optimized code as fast it can. In fact, there is trade-off between generating code quickly, and executing code quickly.
So some engines with more optimzing tiers, tend to focus on executing fast, while it takes long time to generate it. And engines with less optimizing tiers focuses on generating code quickly, while it takes more time to execute because it is less optimizedThere's another trade-off that JS engines consider.
More optimization takes more memory. So, trade-off between optimization and memory usage should be also considered.Objects are just dictionaries which key is string type. String kyes are mapped to something called "property attributes".
Value: the value retuned by accessing property ex) object.x
Writable: whether it can be reassigned
Ennumerable: whether it can be used in loops
Configurable: whether it is deleteable
We can get Property attributes by Object.getownPropertydescript
API.
The problem of storing object this way is that if there are lots of objects, we should allocate memories for each objects, which is wasteful. So, JS engine uses unique mechanism for handling objects.
If objects has same properties, we call that objects have same 'shapes'.(shapes
is synonym to hidden clases
, map
, structure
.)
let object1 = {
x: 1,
y: 2
}
let object2 = {
x: 5,
y: 6
}
Object1 and object2 has same shape.
JS engine uses this concept called shape
internally, to handle objects in more optimized way.
In object, only the values are stored and other property attributes are stored in shape. In shape, instead of the value, offset
property is mapped to property of object. offset
is the index where we can find the value according to the property. For example, for property x
, we can find the value is the 0th place in object.
In above exmaple, a
and b
has same shape. Instead of storing each property attributes to each objects, we store property attributes except value into shape. Using this mechanism, for 1000 objects for same shape, we need only one shape.which can save memory space.
What happens when we start with certain shape and add properties?
let object = {};
object.x = 5;
object.y = 6;
Something called transition chain occurs.
New shapes are introduced when we add new properties. If we want to find the value of propert x
, we walk through the chain until we find the shape that has property x
. Then we look for the offset, which is 0
. 0th value of Object o
is 5. So, o.x = 5.
However, transition chain is still slow if there are multiple shapes included in chain. Time to find the value of property is O(n). So, to make it faster, we introduce ShapeTable
. Shapetable is a dictionary which property maps to the corresponding shape.
Here comes the IC. Ic is ingredient to make JS run fast and the main motivation for having shapes.
JS engine use ICs to memorize information on where to find properties on objects which can reduce the number of lookups.
As we run the above code, interpreter generates the bytecode. Inline Caches are stored in `get_by_id` and has two slots which are uninitialized. As we execute the code with the given object, `get_by_id` looks up the property `x` and finds the offset 0 and looks for the value. After we execute, IC stores and maps the shape and offset which the property was found.For same shape objects, with ICs, JS engine just first compares the shape, and load the value from memorized offset.
Always initialize your object same shape as possible as you can. It boosts the optimization
JS engine generates machine code in runtime.