The V8 Javascript Engine
TL;DR
Hidden Classes, Inline Caches and a Generational Garbage Collector are optimization techniques used by the Google team to speed up the javascript engine.
I’ve always wondered how web browsers use internal compilers and optimizations that make it possible to run JavaScript efficiently. Javascript enables most of the web’s scripting and needs to be executed in such a way that it minimizes the time to output since web pages need to be responsive to the user (although some websites have gone so overboard on the js that it still takes ages).
Javascript is a single-threaded but nonblocking language. This means the heavy lifting of concurrency needs to be handled by the browser. Here’s where V8 comes into the picture.
V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++.
It is mainly employed in Chromium-based browsers.
During development, the team at Google had 3 main goals when it came to design decisions:
- Faster property access
- Faster function calls
- Fast and scalable memory management
Hidden Classes, Inline Caches and a Generational Garbage Collector were the approaches used to reach these goals.
Hidden Classes
Since JS is dynamically typed, the optimization techniques that can be leveraged in statically typed languages are out of reach. The V8 team decided to come up with the notion of hidden classes which served as an internal type only available to the engine. Each variable hence will have an associated hidden class which is built in an incremental method.
For this simple piece of code:
function Polygon(sides, length) {
this.sides = sides;
this.length = length;
}
let p1 = Polygon(3, 5);
let p2 = Polygon(8, 2);
The hidden class is built as:
If you prefer an in-depth example with code, the V8 documentation is a great place to look.
Inline Cache
This is where the magic happens. Since we now have some type of information that we can use to our advantage. The Ignition interpreter usually compiles the function to bytecode. It also serves some profiling data on how often the function is called. If considered a “hot” function, this information can be used by the JIT compiler to generate highly optimized machine code for the function.
This code would obviously run much faster on the CPU and give us better performance as a whole. Genius!
For the more curious bunch, flags like --print-bytecode
and --print-opt-code
will print out the generated code for a Javascript file. Generating the optimized code does however require that the engine deem the function optimization worthy, for which I simply called the function a million times :).
Bytecode
[generated bytecode for function: Polygon (0x03a9654b4d11 <SharedFunctionInfo Polygon>)]
Bytecode length: 14
Parameter count 3
Register count 0
Frame size 0
OSR urgency: 0
Bytecode age: 0
37 S> 000003A9654B5760 @ 0 : 0b 03 Ldar a0
48 E> 000003A9654B5762 @ 2 : 32 02 00 00 SetNamedProperty <this>, [0], [0]
60 S> 000003A9654B5766 @ 6 : 0b 04 Ldar a1
72 E> 000003A9654B5768 @ 8 : 32 02 01 02 SetNamedProperty <this>, [1], [2]
000003A9654B576C @ 12 : 0e LdaUndefined
83 S> 000003A9654B576D @ 13 : a9 Return
Constant pool (size = 2)
000003A9654B5709: [FixedArray] in OldSpace
- map: 0x0042ce2012e1 <Map>
- length: 2
0: 0x03a9654b4b29 <String[5]: #sides>
1: 0x0042ce2055f9 <String[6]: #length>
Handler Table (size = 0)
Source Position Table (size = 12)
0x03a9654b5771 <ByteArray[12]>
Optimized Machine code
Generational Garbage Collection
V8 uniquely handles garbage collection by employing a generation-based technique. There exist two generations, younger and older.
Data is initially stored in the younger generation which is small and collected often. Short-lived objects get reclaimed here. The longer living objects get moved to the older generation which is occasionally collected.
An interesting further exercise for the reader would be to compare how spidermonkey, the Firefox engine, tackles these same issues. Maybe I will write about that in the future as well!
« Slotted Pages in Databases
What I Learnt This Week part 1 »