UPDATED VERSION!
Deep dive into the V8 JavaScript engine, and learn how the runtime treats your code, and how small syntactical changes can have dramatic impacts on performance. Topic cover hidden classes, inline caching, and the compiler optimizer.
8. DYNAMICALLY TYPED VS STATICALLY TYPED
Statically Typed
Checks type annotations at compile time
Enforces type constraints
Dynamically Typed
Checks type annotations at runtime
No type constraints
9. DYNAMICALLY TYPED VS STATICALLY TYPED
Statically Typed
Checks type annotations at compile time
Enforces type constraints
Pros
Compile-time optimizations
Cons
Rigid / requires more boilerplate
Dynamically Typed
Checks type annotations at runtime
No type constraints
Pros
Very flexible / minimizes boilerplate
Cons
No compile-time optimizations
10. DYNAMICALLY TYPED VS STATICALLY TYPED
let myVar;
myVar = 42;
myVar = ’42’;
myVar = { oh: ‘hai’ };
myVar = [ ‘make it stop’ ];
myVar = () => {
return Date.now;
};
myVar.meta = ”I’m so Meta Even This Acronym”;
myVar = myVar();
11. WHAT’S GOING ON?
let myVar;
myVar = 42;
myVar = ’42’;
myVar = { oh: ‘hai’ };
myVar = [ ‘make it stop’ ];
myVar = () => {
return Date.now;
};
myVar.meta = ”I’m so Meta Even This Acronym”;
myVar = myVar();
WTF?
12. OVERVIEW
What is V8?
Dynamically Typed vs Statically Typed
The V8 Solution
Some Practical Measures
16. THE V8 SOLUTION
Build Type
Annotations
Cache Access
Strategies
Optimize
Hidden Classes Inline Caching TurboFan JIT
17. THE V8 SOLUTION
Build Type
Annotations
Cache Access
Strategies
Optimize
Hidden Classes Inline Caching TurboFan JIT
18. THE V8 SOLUTION - HIDDEN CLASSES
let point = {
x: 42,
y: 24
};
Name Offset
x 0
y 1
Point_0
19. THE V8 SOLUTION - HIDDEN CLASSES
let point = {
x: 42,
y: 24
};
Name Offset
x 0
y 1
Point_0
Values Array
42
24
20. THE V8 SOLUTION - HIDDEN CLASSES
let point = {
x: 42,
y: 24
};
point.z = 66;
Name Offset
x 0
y 1
Point_0
Values Array
42
24
21. THE V8 SOLUTION - HIDDEN CLASSES
let point = {
x: 42,
y: 24
};
point.z = 66;
Name Offset
x 0
y 1
Point_0
Values Array
42
24
Name Offset
x 0
y 1
z 2
Point_1
Values Array
42
24
66
22. THE V8 SOLUTION - HIDDEN CLASSES
function(que) {
let point = (que)
? { x: 42, y: 24 }
: { y: 42, x: 24 };
return point;
};
Name Offset
x 0
y 1
Point_0
Values Array
42
24
Name Offset
y 0
x 1
Point_1
Values Array
24
42
23. THE V8 SOLUTION - HIDDEN CLASSES
function(que) {
let point = (que)
? { x: 42, y: 24 }
: { x: 42, y: 24 };
return point;
};
Name Offset
x 0
y 1
Point_0
Values Array
42
24
Values Array
24
42
24. THE V8 SOLUTION - HIDDEN CLASSES
function(que) {
let point = (que)
? { x: 42, y: 24, z: 66 }
: { x: 24, y: 42, z: 66 };
return point;
};
Name Offset
x 0
y 1
z 2
Point_0
Values Array
42
24
66
Values Array
24
42
66
25. THE V8 SOLUTION
Build Type
Annotations
Cache Access
Strategies
Optimize
Hidden Classes Inline Caching TurboFan JIT
26. THE V8 SOLUTION - INLINE CACHING
Variable
(A)
Hidden Class
(string)
Inline
Cache
27. THE V8 SOLUTION - INLINE CACHING
Variable
(A)
Hidden Class
(string)
Variable A
string
shortcut
Inline
Cache
28. THE V8 SOLUTION - INLINE CACHING
Variable
(A)
Hidden Class
(string)
Variable A
string
shortcut
Hidden Class
(numeric)
Inline
Cache
29. THE V8 SOLUTION - INLINE CACHING
Variable
(A)
Hidden Class
(string)
Variable A
string
shortcut
Hidden Class
(numeric)
Variable A
numeric
shortcut
Inline
Cache
30. THE V8 SOLUTION - INLINE CACHING
function square(a) {
return a * a;
}
let value0 = square(2);
31. THE V8 SOLUTION - INLINE CACHING
function square(a) {
return a * a;
}
let value0 = square(2);
Shortcut to numeric ”a”
32. THE V8 SOLUTION - INLINE CACHING
function square(a) {
return a * a;
}
let value0 = square(2);
let value1 = square(3);
let value2 = square(4);
let value3 = square(5);
Shortcut to numeric ”a”
Monomorphic
33. THE V8 SOLUTION - INLINE CACHING
function square(a) {
return a * a;
}
let value0 = square(2);
let value1 = square(‘2’);
Shortcut to numeric ”a”
Shortcut to string ”a”
Polymorphic
34. THE V8 SOLUTION - INLINE CACHING
function square(a) {
return a * a;
}
let value0 = square(2);
let value1 = square(‘2’);
let value2 = square(Date.now);
let value3 = square({
valueOf: () => { return 2; }
});
Shortcut to numeric ”a”
Shortcut to A_0 object ”a”
Shortcut to string”a”
Shortcut to Date”a”
Megamorphic!
35. THE V8 SOLUTION - INLINE CACHING
function square(a) {
return a * a;
}
let value0 = square(2);
let value1 = square(‘2’);
let value2 = square(Date.now);
let value3 = square({
valueOf: () => { return 2; }
});
Shortcut to numeric ”a”
Shortcut to A_0 object ”a”
Shortcut to string”a”
Shortcut to Date”a”
Megamorphic! Megamorphic global hashtable
36. THE V8 SOLUTION - INLINE CACHING
function square(a) {
return a * a;
}
const value0 = square(2);
const value1 = square(parseInt(’3’));
Monomorphic!
37. THE V8 SOLUTION
Build Type
Annotations
Cache Access
Strategies
Optimize
Hidden Classes Inline Caching TurboFan JIT
39. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
Functionally similar to the “full compiler”
40. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
Functionally similar to the “full compiler”
Builds a graph of internal representation (IR) of code
41. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
Functionally similar to the “full compiler”
Builds a graph of internal representation (IR) of code
Optimization engine applies various rules as the
IR is constructed, and un-optimized machine code is
replaced with optimized machine code
42. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
const a = 2;
const b = 2;
const sum = a + b;
43. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
const a = 2;
const b = 2;
const sum = a + b;
mov eax, a
mov ebx, b
call RuntimeAdd
44. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
const a = 2;
const b = 2;
const sum = a + b;
mov eax, a
mov ebx, b
add eax, ebx
45. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
const a = 2;
const b = 2;
const sum = a + b;
mov eax, a
mov ebx, b
add eax, ebx
Canonicalization, architecture-specific optimizations, dead code removal, global
value numbering, loop invariant code motion, redundant bounds check elimination,
etc.
46. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
Some code cannot be optimized...
47. THE V8 SOLUTION - THE OPTIMIZING COMPILER (TURBOFAN)
Some code cannot be optimized...
Reassigning a defined parameter while mentioning arguments
Object literals that contain __proto__, or get or set declarations
Functions that use debugger, eval(), or with statements
for-in statements where they key is not a local variable
for-in statements on arrays
Infinite loops with deep logic exit conditions
48. OVERVIEW
What is V8?
Dynamically Typed vs Statically Typed
The V8 Solution
Some Practical Measures
49. SOME PRACTICAL MEASURES
Define variables with as much information as
possible...
Do:
const point = {
x: 42,
y: 24
};
Don’t:
const point = {}
point.x = 42;
point.y = 24;
50. SOME PRACTICAL MEASURES
Perform your own coercion on variables before passing to
functions...
Do:
const a = ’24’;
square(parseInt(a));
Don’t:
const a = ’24’;
square(a);
51. SOME PRACTICAL MEASURES
Don’t reassign a defined parameter while mentioning arguments...
Do:
function argsReassign(a, _b) {
let b = _b;
if (arguments.length < 2) b = 5;
}
Don’t:
function argsReassign(a, b) {
if (arguments.length < 2) b = 5;
}
52. SOME PRACTICAL MEASURES
Don’t use __proto__, get, or set in object literals...
Don’t:
function someUnoptimizableFunction() {
return {
__proto__: 42,
get a() { return ‘a’; }
set a(val) { this._a = val; }
};
}
53. SOME PRACTICAL MEASURES
Don’t use debugger...
Don’t:
function someUnoptimizableFunction() {
if (process.env.NODE_ENV === ‘develop’) {
debugger;
}
}
54. SOME PRACTICAL MEASURES
Don’t use with...
Don’t:
function someUnoptimizableFunction(hello) {
with (hello) {
console.log(world);
}
}
55. SOME PRACTICAL MEASURES
Don’t use eval...
Don’t:
function someUnoptimizableFunction(script) {
return eval(script);
}
56. SOME PRACTICAL MEASURES
Don’t use for-in statements on arrays...
Do:
function optimizable() {
const obj =[1, 1, 2, 3, 5, 8];
for (let i = 0; i < obj.length; i++);
}
Don’t:
function unoptimizable() {
const obj =[1, 1, 2, 3, 5, 8];
for (const item in obj);
}
57. SOME PRACTICAL MEASURES
Don’t create infinite loops with deeply nested exit conditions...
Do:
function optimizable() {
while (someExitCondition()) {
// do stuff
}
}
Don’t:
function unoptimizable() {
while (true) {
if (someExitCondition()) {
break;
}
}
}
58. TURBOFAN IS RAPIDLY EVOLVING
The switch from CrankShaft to TurboFan represents a complete redesign of the optimizing compiler.
TurboFan GREATLY simplified the code.
Consequently, the project is seeing more contributors.
Compiler bailouts are being fixed.
This presentation may be out of date by the time you read it.
Follow the V8 project on GitHub to keep up-to-date on new features
https://github.com/v8/v8
59. SOME PRACTICAL MEASURES
“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of
all evil. Yet we should not pass up our opportunities in that critical 3%”
“In established engineering disciplines a 12% improvement, easily obtained, is never considered marginal
and I believe the same viewpoint should prevail in software engineering”
– Donald Knuth
60. THE END
Thank You!
Dan Fields
https://www.linkedin.com/in/danielsfields
https://github.com/dsfields
@danielsfields
Editor's Notes
This talk is primarily based on how the V8 runtime handles JavaScript’s dynamic type system.
The content is framed to compare dynamically typed languages against statically typed languages.
How V8 attempts to provide the similar optimizations found in statically typed language compilers.
And, finally, some simple things you can do to take advantage of V8’s optimizer.
V8 is the JavaScript engine built into the Chrome web browser. It is also the default JavaScript runtime used by Node.js. V8 is a JavaScript parser, compiler, and runtime all in one.
JavaScript is a dynamic and interpreted language.
As we see, this allows us to write code that, from our perspective, is conceptually the same, and only syntactically different.
But what about from V8’s perspective?
JavaScript is a dynamic and interpreted language.
As we see, this allows us to write code that, from our perspective, is conceptually the same, and only syntactically different.
But what about from V8’s perspective?
The differences between static and dynamic languages are pretty straight forward.
They can be summed up by the manner in which they handle type annotations.
Type annotations are meta data about data types, and serve as hints about how the runtime should handle a variable in a particular context.
These differences come with tradeoffs.
In a statically typed language, the compiler knows exactly how to handle a particular variable.
The compiler can then emit machine code that knows exactly how to allocate and address variables in memory, and it can more easily take advantage of architecture-specific optimizations.
The trade-off here is, you’ll inevitably have to write more code to handle various type differences for different situations.
In other words, more boilerplate.
Since JavaScript is dynamic, we don’t have the problem.
However, no compile-time optimizations, which can make things slower.
To illustrate this point, consider this example.
myVar can basically be anything.
And V8 will handle it.
As a consequence, it becomes quite difficult for a compiler to make assumptions about a piece of code is, at a low level, going run.
JavaScript is syntactically a fairly lean language, but the reality is, the language is remarkably complex.
This makes performing even simple operations surprisingly difficult.
To understand why, we’ll need to dive a bit deeper into what V8 is doing under the hood.
Back to the notion of type annotations for a moment. Even though V8 can’t know what the type of a variable is at compile time, it still needs a way to know about a type.
So, it will attempt to build type annotations as it executes.
Now, V8 can utilize this information to determine an access strategy for in-memory data to perform a specific operation.
No more complex decision trees.
Finally, we are able to run an optimizing compiler on on our code.
Along the way, V8 uses various constructs designed to efficiently solve their respective problem.
So lets dive into the design of these components, and see how they can influence the patterns and practices we should be following when structuring code.
As mentioned, hidden classes solve the type annotation problem.
On the left here, we have a small bit of code where we’re declaring an object with two properties, x and y.
On the right, we have table that shows us what the hidden class definition of the type of our variable point looks like.
The hidden class table defines the names of our object’s properties, and an offset value; which is a reference to the order that property is defined on the hidden class.
Our point variable is an instance of our hidden class Point_0.
And instance values are then stored in an array, where each value is mapped back to their respective property based on their offset in the array.
All very simple.
Now the caveat: hidden classes are immutable.
But a variable’s type is dynamic.
So, what happens when the type changes?
The answer is, a new hidden class is created.
So, you can start to see how the behavior of hidden classes dictate how we should structure our code.
Even the order in which we define our object properties can result in new hidden classes being created.
The consequences of having multiple hidden classes can increase memory consumptions.
But even more importantly, the different code paths can now no longer utilize the same optimized code.
We’ll get into what exactly that means in a moment.
So, make sure you declare your variables and objects consistently.
And include as much as you can at the time an object instance is declared.
We now have the type annotation information we need to figure out how to efficiently load data when performing an operation.
So, now that we have our type annotations, the runtime needs a way to reference them in memory. Enter inline caching.
The easiest way to think of an inline cache is a shortcut to finding a variable value in memory.
Here we have variable A, which is of a hidden class of type string.
This information allows us to reference a shortcut to specific spot in memory.
So, what happens if variable A isn’t referencing its string hidden class anymore?
Instead it’s referencing its numeric valued hidden class.
Simple, we end up with a cache miss.
And a new entry in the cache is created.
And we’re invoking our function, and passing in the numeric value 2.
The runtime either gets or creates an inline cache entry for numeric “a” that allows us to reference the correct value in memory so we can complete our multiplication operation.
Now lets say we continue calling our function more or less the same way.
Our function only needs one inline cache entry, and has a single, unchanged path of execution.
We call this a monomorphic function.
Lets change the scenario a bit.
In this case, we’re passing in a string.
So, we have a cache miss, and we create a new shortcut entry in the inline cache.
Our code now has multiple inline cache entries, and, as a result, a logical path of execution that can change depending on the hidden class of our “a” parameter.
Our function is now polymorphic.
And we can continue on adding shortcut entries into our inline cache, something interesting happens.
V8 has a hard maximum limit of 3 entries in the inline cache.
This is a safeguard against ensuring all of our many inline caches don’t grow indefinitely.
We now have a megamorphic function.
This is not good.
Because instead of using an inline cache, we now must use a global hashtable to store or shortcuts.
In addition to being considerably slower than an inline cache lookups, the hashtable is a fixed size.
Entries follow a LRU strategy for cache ejection.
That means our program can be further slowed down due to frequent cache misses.
Plan accordingly.
Create separate functions to accommodate the different hidden classes that result in your code.
The reason for this, is because of the final component we need to talk about.
Now that V8 has type information and access strategies, the code can now be optimized.
I don’t want to dive too deep into the design of TurboFan, as that could its own series of talks.
And it’s not necessary for understanding how to take advantage of TurboFan’s code optimizations.
So, lets just walk through a high-level description, and see an example.
Functionally similar to the “full compiler” that generated the original machine code when I application first started running.
The full compiler emits good machine code, but it can’t emit optimal code.
You can think of
You can think of
Lets look at an example.
Simple addition of two numeric variables.
The full compiler may emit machine code that looks like this.
What’s important to note here is the call to RuntimeAdd.
This is V8’s internal function for summing to variables.
It contains all of the coercion routines and inline cache look ups necessary to perform the operation on an unknown data type.
Now lets assume that our code has remained monomorphic.
Meaning, contextually, a and b have always been integers.
And there is only one hidden class and inline cache for each.
TurboFan can now make some assumptions and replace RuntimeAdd with the more optimal add instruction.
And when I say more optimal, I mean possibly on the order of 100 times faster.
There are many many more optimizations that TurboFan can perform on your code.
A bailout is a situation where the optimizing compiler encounters a piece of code where it just says, “I don’t know what to do with this, so I’m not going to try.”
That function will never become optimized.
Functions that call un-optimizable functions cannot be optimized
So, be careful, or you can end up with large swaths of your application that bailout of optimization entirely
To understand why, we’ll need to dive a bit deeper into what V8 is doing under the hood.
A couple of quotes from the master.
The information presented here today is just another tool to be utilized while designing code.
They cannot be used as hard-and-fast rules.
Learn how to use them to create guidelines for how to compose code.
Then learn how to break them when necessary.