神刀安全网

How V8 optimises JavaScript code?

In myprevious article, I was talking about NodeJS and why it’s fast. Today, I want to talk about V8.

I suppose, some of you heard that JavaScript executes as fast as C++. Some of you don’t understand how it’s even possible. JavaScript is a dynamically-typed language with Just in Time (JIT) compilation, when C++ is static-typed language with Ahead of Time (AoT) compilation. And somehow, optimised JavaScript code executes a little bit slower than C++ or even with the same speed.

To understand why that is, you need to know some basics of V8 implementation. It’s a huge topic, so I’m only going to explain key features of V8 in this post. If you want more details, such as hidden classes, SSA, IC, etc… they will be in my next article.

AST

Everything starts from your JavaScript code and its Abstract Syntax Tree (AST).

In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code. The syntax is “abstract” in not representing every detail appearing in the real syntax.

What can we do with AST? Before doing any optimisations on your code, it first needs to be transformed into a lower level representation. That’s essentially what AST is. AST contains all the required information about your code for V8 to analyse.

In the beginning, all of your code is transformed into AST and going through full-codegen compiler.

Full-Codegen Compiler

The main purpose of this compiler is to compile your JavaScript code to native code as quick as possible, without optimisations. It handles all of the various cases and includes some type-feedback code that collects information about data types seen at various points of your JavaScript function.

It takes AST, walks over the nodes and emits calls to a macroassembler directly. Result of this operation is generic native code.

That’s it. Nothing special. No optimisations are performed here, complicated cases of your code are handled by emitting calls to runtime procedures, all local variables are stored on the heap, etc…

The most interesting part is when V8 sees that your function is hot and it’s time to optimise it. That’s when Crankshaft compiler comes into play.

Crankshaft Compiler

As I mentioned before, full-codegen compiler generates generic native code with the code that collects type-feedback information about your functions. When function becomes hot ( hot function is a function that is called very often ), Crankshaft can use AST with that information to compile optimised code for your function. Afterwards, optimised function will replace un-optimised using on-stack replacement (OSR).

But… optimised function doesn’t cover all the cases. If something went wrong with types, for instance, function starts returning float number instead of integers, optimised function will be deoptimised and replaced with old un-optimised code. We don’t want that, do we?

For example, you have a function that adds two numbers:

const add = (a, b) => a + b;
// Let's say we have a lot of calls like this
add(5, 2);
// ...
add(10, 20);

When you call this function many times with integers only, type-feedback information consists of information that our a and b arguments is integers. Using that information and AST of this function, Crankshaft can optimise this function. But, everything would break if you made a call like this:

add(2.5, 1); // float number as the first argument

Based on the previous type-feedback information, Crankshaft makes assumption that only integers are going through this function, but we’re passing it a float number. There is no optimised code to handle that case, so it just de-optimises.

You might ask, how does all this magic work in Crankshaft? Well, there are a few parts which work in Crankshaft compiler together:

  • Type feedback ( already discussed above );
  • Hydrogen compiler;
  • Lithium compiler;

Hydrogen Compiler

Hydrogen takes AST with type-feedback information as its input. Based on that information, it generates high-level intermediate representation (HIR) which consists of a control-flow graph (CFG) in static-single assignment form (SSA).

During the generation of HIR, several optimisations are applied, such as constant folding, method inlining, etc… ( I will talk about V8 optimisation tricks in the next article ).

The result of these optimisations is an optimised control-flow graph (CFG) that is used as input to the next compiler — Lithium compiler, which generates the actual code.

Lithium Compiler

Lithium compiler takes optimised HIR and translates it to machine-specific low-level intermediate representation (LIR). The LIR is conceptually similar to machine code, but still mostly platform-independent.

During LIR generation Crankshaft still can apply some low-level optimisations to it. After LIR was generated, Crankshaft generates a sequence of native instructions for each Lithium instruction.

Afterwards, the resulting native instructions get executed.

Summary

This is the magic that happens in V8 internals to optimise your JavaScript, to make it execute as fast as C++. Still, you need to understand, that badly written JavaScript will not be optimised and you don’t get the benefits of optimisation.

I will talk about V8 optimisation tricks, ways to profile bottlenecks in your code and how to look for de-optimisations, investigating the control-flow graph (CFG) in the following articles.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » How V8 optimises JavaScript code?

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮