G++ execution result:
Rhino execution result:
As a clarification, the scenario has its limitations and the result may vary given different configurations.
To answer this question, I start out by asking another question.
In static OO language, a particular property is always found at a known offset in instances of a particular class, so are the method calls. What that means is the properties and functions’ location of a class in the heap is predictable given a specific type information.
Rather than re-inventing the wheel, just embrace what Google has come up with, V8.
What makes V8 really stand out?
There are several areas V8 does differently. By understanding the most distinct features listed below, I hope this can give a brief insight of V8.
1. Hidden classes
One of the key optimisations in V8 is introducing the concept of hidden class.
V8 internally creates
Given the following example:
The Point object will be created twice to get
But instead of having
p2 as completely independent objects, V8 creates a chain of hidden classes, and identifies which hidden class should it use as a template to create
In the process of creating
p1, the chain of hidden classes is generated:
Point -> Point(x) -> Point(x, y). And when it comes to create
p2, V8 will find the hidden class with the same number of properties as given in p2, and uses the hidden class for
2. Inline caching
Inline caching (IC)is type dependent small chunk of code, which knows how to perform an operation (read or write object properties, or making function calls) given inputs of hidden class. It validates type assumptions to choose a hidden class to use or to generate new IC with new type information.
To be more specific, there are four states of inline cache in V8:
- Uninitialised: it is used to tag a complete new object. A dynamic lookup is required when generating the code, then the state will be changed to pre-monomorphic state.
- Pre-monomorphic: the object of this state behaves mostly like uninitialised state, a dynamic lookup is also performed. But it rewrites the state to monomorphic in the end. The reason of having this state is to avoid generating customised optimised machine code for the stuff that’s used only once.
- Monomorphic: this state records the hidden class of an object (on the second property access), so that it can be blazing fast to access from this point onwards.
- Megamorphic: it generally locks an object to the state and requires dynamic lookup every time, until the object is garbage collected and re-assigned a different state.
3. Garbage collection
As one of the crucial components of a compiler, V8 also re-invents the garbage collector, a
generational garbage collector.
The heap is split into two separate areas:
- Young generation: where the newly allocated objects are stored. Because most objects created are short lived or used only once, the space can be collected frequently.
- Old generation: for the objects that survived the selection mechanism, they get promoted into the old generation, which is likely to live longer. Inside old generation, it is further split into different space: code space (for executable code), old data space (for objects have no pointer to young generation), large object space (to avoid moving of objects), hidden class space (a fixed size area with special GC process) and old pointer space (rest of the old objects).
In order to reclaim storage in smaller chunks and more frequent basis instead of collecting the entire heap at once, there are three different kinds of garbage collectors:
- Scavenge collector: which targets young generation. It collects frequently, and promotes objects to old generation. It generally has a very short pause time, but depending on size of live data in young generation)
- Mark-sweep collector: which targets the entire heap. The pause time depends on size of heap.
- Mark-sweep-compact collector: which targets the entire heap to eliminate the fragmentation problem of the heap. And it is likely to have a long pause time.
V8 has two different kinds of compilers to make the performance leap:
- Full-codegen compiler generates good code in compile time, without any knowledge about type information. But this compiler uses ICs to refine its type knowledge on the fly.
- Crankshaft compiler, or a optimising compiler, re-compiles hot functions to generate even better code. It takes type information collected in ICs (type feedback) and inlines operations speculatively, and monomorphic functions and constructors entirely.
By running the prime number calculator in Node, here is the result:
Still almost twice the time as C spent, but it is a big leap from Rhino.
It sounds great, how can I use V8?
Some tips and tricks
- Initialise all object members in constructor functions to teach V8 which hidden classes to have after the object is constructed.
- Always initialise object members in the same order, because V8 will create different branch of hidden classes if members of a same object are constructed differently.
- Use numeric values that can be represented as 31-bit signed integers for performance critical calculation to take advantage of V8’s tagging mechanism. Because if the value exceeds 31-bit signed, it will be converted into a double value.
- Use contiguous keys starting at 0 for Arrays to take advantage of Fast elements’ linear lookup.
- Don’t delete elements in arrays, especially numeric arrays. Because it makes an array a sparse array, and V8 will switch to Dictionary mode to do the hash table lookup.
- Don’t load uninitialised or deleted elements.
- Preallocate small arrays to correct size before using them
- Don’t store non-numeric values (objects) in numeric arrays, which could cause a type switch.
So break free and have fun!