Foreword
Daniel Clifford gave a great talk on Google I / O, dedicated to the features of optimizing JavaScript code for the V8 engine. Daniel urged us to strive for greater speed, carefully analyze the differences between C ++ and JavaScript, and write code, keeping in mind how the interpreter works. I have compiled in this article a summary of the most important points of Daniel's speech, and I will update it as the engine changes.
Top tip
It is very important to give any performance tips in context. Optimization often becomes an obsessive habit, and deep immersion in the wilds can actually distract from more important things. You need a holistic view of web application performance - before focusing on these optimization tips, you should analyze your code with tools like
PageSpeed and first achieve a good overall result. This will help avoid premature optimization.
The best strategy for creating a fast web application is as follows:
- Think of everything before you run into problems.
- Understand carefully and get to the heart of the problem.
- Correct only what matters.
To stick to this strategy, it is important to understand how V8 optimizes JS, to imagine how everything happens at run time. It is also important to have the right tools. In his speech, Daniel devoted more time to developer tools; In this article, I mainly look at the features of the V8 architecture.
')
So let's get started.
Hidden Classes
At compile time, the type information in JavaScript is very limited: at runtime, types can change, so it is only natural to expect that when compiling it is difficult to make assumptions about them. The question arises - how in such conditions can you at least get closer to the speed of C ++? However, V8 manages to create hidden classes for objects at runtime. Objects that have the same class share the same optimized code.
For example:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(11, 22); var p2 = new Point(33, 44);
As long as the “
.z
” property was not added to
p2
,
p1
and
p2
inside the compiler had the same hidden class, and V8 could use the same optimized machine code for both objects. The less often you change the hidden class, the better the performance will be.
Findings:
- Initialize all the objects in the constructors so that they change as little as possible later on.
- Always initialize the properties of an object in the same order.
Numbers
V8 keeps track of how you use variables, and uses the most efficient view for each type. Changing the type can be quite expensive, so try not to mix floating-point numbers and integers. In general, it is better to use integers.
For example:
var i = 42;
Findings:
- Try to use 31-bit signed integers wherever possible.
Arrays
V8 uses two types of internal array representation:
- Real arrays for compact sequential sets of keys.
- Hash tables in other cases.
Findings:
- You should not force arrays to jump from one category to another.
- Use continuous indexing starting from 0 (exactly as in C).
- Do not pre-fill large arrays (containing more than 64K elements) - this will not work.
- Do not remove elements from arrays (especially numeric).
- Do not refer to uninitialized or deleted items. Example:
a = new Array(); for (var b = 0; b < 10; b++) { a[0] |= b;
Arrays of numbers with double precision work the fastest - the values ​​in them are unpacked and stored as elementary types, and not as objects. Thoughtless use of arrays can lead to frequent unpacking:
var a = new Array(); a[0] = 77;
It will be much faster like this:
var a = [77, 88, 0.5, true];
In the first example, individual assignments occur sequentially, and at the moment when a[2]
takes a value, the compiler converts a
into an array of decompressed numbers with double precision, and when a[3]
initialized with a non-numeric element, the inverse transformation occurs. In the second example, the compiler will immediately select the desired type of array.
In this way:
- Small fixed arrays are best initialized using an array literal.
- Fill small arrays (<64K) before use.
- Do not store non-numeric values ​​in numeric arrays.
- Try to avoid conversions when initializing not through literals.
Compiling javascript
Although JavaScript is a dynamic language and was originally interpreted, all modern engines are actually compilers. Two compilers work in V8 at once:
- A basic compiler that generates code for the entire script.
- An optimizing compiler that generates very fast code for the “hottest” sites. This compilation takes longer.
Basic compiler
In V8, he is the first to start processing all the code and run it as quickly as possible. The code generated by it is almost not optimized - the basic compiler makes almost no assumptions about types. During execution, the compiler uses inline caches, in which code-dependent portions of code are stored. When this code is run again, the compiler checks the types used in it before selecting the appropriate version of the ready-made code from the cache. Therefore, operators that can work with different types run slower.
Findings:
- Prefer monomorphic operators polymorphic.
An operator is monomorphic if the hidden type of operands is always the same, and polymorphic if it can change. For example, the second call to
add()
makes the code polymorphic:
function add(x, y) { return x + y; } add(1, 2);
Optimizing compiler
In parallel with the operation of the basic compiler, the optimizing compiler recompiles the “hot”, that is, those that are often executed, code segments. It uses type information accumulated in inline caches.
The optimizing compiler tries to build functions into the call sites, which speeds up execution (but increases memory consumption) and allows for additional optimizations. Monomorphic functions and constructors can easily be embedded entirely, and this is another reason why we should strive to use them.
You can see what exactly is optimized in your code using the standalone version of the d8 engine:
d8 --trace-opt primes.js
(the names of the optimized functions will be displayed in stdout
)Not all functions can be optimized. In particular, the optimizing compiler skips any functions containing
try/catch
blocks.
Findings:
If you need to use a
try/catch
, place performance-critical code outside. Example:
function perf_sensitive() {
Perhaps the situation will change in the future, and we will be able to compile
try/catch
blocks with an optimizing compiler. You can see exactly which functions are ignored by specifying the
--trace-bailout
when running d8:
d8 --trace-bailout primes.js
Deoptimization
Code generated by the optimizing compiler is not always faster. In this case, the original, non-optimized version is used. Unsuccessfully optimized code is thrown away, and execution continues from the appropriate place in the code created by the base compiler. This code may be optimized again soon if circumstances permit. In particular, changing hidden classes inside an already optimized code leads to de-optimization.
Findings:
- Avoid changing hidden classes in optimized functions.
You can see exactly which functions are being deoptimized by running d8 with the
--trace-deopt
:
d8 --trace-deopt primes.js
Other V8 Tools
The above functions can be transferred to Google Chrome at startup:
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt --trace-bailout
There is a profiler in d8 too:
d8 primes.js --prof
The sampler profiler d8 takes pictures every millisecond and writes to
v8.log
.
Summary
It is important to understand how the V8 engine works in order to write well-optimized code. And do not forget about the general principles described at the beginning of the article:
- Think of everything before you run into problems.
- Understand carefully and get to the heart of the problem.
- Correct only what matters.
This means that you have to make sure that it is in JavaScript, using tools such as PageSpeed. It may be worth getting rid of DOM calls before looking for bottlenecks. I hope that Daniel’s speech (and this article) will help you to better understand the work of V8, but do not forget that it is often more useful to optimize the algorithm of the program, rather than adjust to a specific engine.
References: