• How JavaScript Works: Inside the V8 Engine + 5 Tips on How to Write Optimized Code
  • Originally written by Alexander Zlatkov
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Spring Snow
  • Proofreader: PCAaron Raoul1996

How JavaScript works: 5 Tips for Optimizing Code in V8 engines

A few weeks ago we started a series of articles designed to dig deeper into JavaScript and how it works. Understanding the underlying build and how it works can help us write better code and applications.

The first article focuses on an overview of the engine, runtime, and call stack. The second article will delve into the bowels of Google’s JavaScript V8 engine. We also provided some quick tips on how to write better JavaScript code — best practices that our SessionStack development team follows when developing the product.

An overview of the

A JavaScript engine is a program or interpreter that executes JavaScript code. JavaScript engines can be implemented as standard interpreters or just-in-time compilers that somehow compile JavaScript into bytecode.

Here are some of the most popular projects implementing JavaScript engines:

  • V8 – an open source engine developed by Google and written in C++
  • Rhino – Managed by the Mozilla Foundation, Rhino is an open source engine developed entirely in Java
  • SpiderMonkey – The first JavaScript engine that supported Netscape Navigator at the time and is now the engine for Firefox
  • JavaScriptCore – An open source engine developed by Apple for Safari and marketed as Nitro.
  • KJS – The engine for KDE, originally developed by Harri Porten for the KDE project’s Konqueror Web browser
  • Chakra (JScript9) – IE engine
  • Chakra (JavaScript) – Microsoft Edge engine
  • Nashorn — Open source engine, developed by Oracle’s Java Language tools group and part of OpenJDK
  • JerryScript – This is a lightweight engine for the Internet of Things

Why build a V8 engine?

V8 engine is an open source engine developed by Google in C++, which is also used in Google chrome. Unlike the other engines, V8 is also used to run Node.js.

V8 was originally designed to improve the performance of JavaScript execution inside browsers. For faster speeds, V8 compiles JavaScript code into more efficient machine code rather than using an interpreter. Like many modern JavaScript engines such as SpiderMonkey or Rhino (Mozilla), it compilers JavaScript code into machine code using a just-in-time compiler. The main difference is that V8 does not generate bytecode or any intermediate code.

V8 used to have two compilers

Before the v5.9 version of V8 came out (released earlier this year) there were two compilers:

  • Full-codegen – a simple and very fast compiler that generates simple but relatively slow machine code.
  • Crankshaft – a more complex (just-in-time) optimization compiler that generates highly optimized code.

The V8 engine also uses multiple threads internally:

  • The main thread does what you expect it to do: take your code, compile it, and execute it
  • A separate thread is also used for compilation so that the main thread can continue executing, while the former optimizes the code
  • aProfilerThe thread, which tells the runtime which methods we’re spending a lot of time on, so thatCrankshaftYou can optimize them
  • There are also threads that handle garbage collection scans

When executing JavaScript code for the first time, V8 uses full-CodeGen to translate the parsed JavaScript code directly into machine code without any conversion. This allows it to start executing machine code very quickly, and note that V8 does not use any intermediate bytecode representation, thereby eliminating the need for an interpreter.

When your code has been running for a while, the profiler thread has collected enough data to tell the runtime which method should be optimized.

The Crankshaft then started optimizing in another thread. It converts the JavaScript abstract syntax tree into an advanced static unit allocation representation (SSA) called Hydrogen, and attempts to optimize the Hydrogen diagram. Most optimizations are done at this level.

Inlining

First time optimization is embedding as much code as possible in advance. Code embedding is replacing the place where a function is used (the line where the function is called) with the calling body. This simple step will make the following optimizations more useful.

Hidden Classes

JavaScript is a prototype-based language: no classes or objects are created by cloning. JavaScript is also a dynamic language, which means you can easily add or remove properties from objects after instantiation.

Most JavaScript interpreters use dictionary-like structures (based on hash functions) to store the in-memory location of object attribute values. This structure makes retrieving a property value in JavaScript much more computationally expensive than in non-dynamic languages like Java or C#. In Java, all property values are determined in a fixed object layout prior to compilation, and cannot be dynamically added or removed at runtime (of course, C# has dynamic typing, but that’s another topic). Thus, attribute values (or Pointers to these attributes) can be stored in memory as consecutive buffers with a fixed offset between each value. The length of the offset can be easily determined based on the property type, which is impossible in JavaScript because the property type can change at run time.

Since it is inefficient to use dictionaries to find the location of object attributes in memory, V8 takes a different approach: hiding classes. Hidden classes work much like fixed object layouts (classes) used in languages such as Java, except that they are created at run time. Now, let’s see what they actually look like:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1.2);
Copy the code

Once “new Point(1, 2)” is called,V8 will create a hidden class called “C0”.

At this Point, Point has not defined any properties, so “C0” is empty.

When the first statement “this.x = x” is executed (in the “Point” function), V8 will create a second hidden class called “C1” based on “C0”. “C1” describes the location in memory of the attribute value X relative to the object pointer. In this example, the “x” is offset to 0, which means that when a point object is treated in memory as a contiguous buffer, its first offset corresponds to an attribute of “x”. V8 also updates “C0” with class transformation, and if an attribute “x” is added to the point object, the hidden class is switched from “C0” to “C1”. So, the hidden class for this Point object is now “C1”.

Every time a new property is added to the object, the old hidden class is updated to a new hidden class through a transformation path. Hidden class conversions are important because they allow objects created in the same way to share hidden classes. If two objects share a hidden class and add the same attributes to them, the hidden class transformation ensures that both objects get the new hidden class and the optimized code associated with it.

This process is repeated when the statement “this.y = y” is executed (again, inside the Point function, after the statement “this.x = x”).

A new hidden class “C2” is created, if the attribute “y” is added to the Point object (which already contains the attribute “X”), the same process, the type conversion is added to “C1”, then the hidden class starts updating to “C2”, And the hidden class of the Point object will be updated to “C2”.

Hidden class conversions change based on the order in which attributes are added to the object. Let’s look at the following snippet of code:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1.2);
p1.a = 5;
p1.b = 6;
var p2 = new Point(3.4);
p2.b = 7;
p2.a = 8;
Copy the code

Now, you might think that P1 and P2 use the same hidden classes and class conversions. In fact, for P1, attribute “a” is added first, followed by attribute “b”. For P2, “B” is assigned first, followed by “A”. Therefore, P1 and P2 will end up in different class-conversion paths, with different hidden classes. In fact, as we can see in both examples, it is best to initialize the dynamic properties in the same order so that the hidden classes can be reused.

Inline caching

V8 also uses another technique called inline caching to optimize dynamically typed languages. Inline caching depends on the observation that repeated calls to the same method occur on objects of the same type. For a deeper look at inline caching, see here.

Let’s take a look at the basic concepts of inline caching (if you don’t have time to read the in-depth interpretation above).

So how does it work? V8 maintains a cache of object types that were passed as arguments in the most recent method call, and V8 then uses this information to predict what type of object will be passed as arguments again in the future. If V8 makes a good prediction of the type of object passed to the method, it can bypass the calculation of obtaining the object’s attributes and instead use the information stored in the previous search for the object’s hidden class.

So how do the concepts of hidden classes and inline caching fit together? Whenever a method is called on a particular object, the V8 engine looks for the object’s hidden class to determine the offset value for that particular property. When the same method is successfully called twice on the same hidden class, V8 skips looking for the hidden class and adds the attribute’s offset value to the pointer to the object itself. For all future calls to this method, the V8 engine will assume that the hidden class has not changed and will instead jump directly to the location in memory of the particular property, using the offset value stored during the previous lookup. This greatly improves the speed of V8 execution.

Also, inline caching is why it is so important for objects of the same type to share hidden classes. If we create two objects of the same type using different hidden classes (as we did earlier), V8 cannot use inline caching because even though the two objects are identical, their corresponding hidden classes assign different offsets to their attributes.

The two objects are essentially the same, but the attributes “A” and “b” are created in different order

Compile to machine code

Once the Hydrogen graph was optimized, the Crankshaft would take the graph down to a lower-level representation called Lithium. Most Lithium implementations are structure-specific. Register allocation occurs at this level.

Finally, Lithium is compiled into machine code. OSR then begins: on-stack replacement, a run-time technique for replacing the running stack frame. We might run it when we start compiling and optimizing an obviously time-consuming method. V8 does not throw away its previously slow code and then execute optimized code. Instead, V8 transforms the context (stack, register) of the code so that it can switch to the optimized version on its way to executing the slower code. This is a very complex task, given that V8 has embedded the code in other optimizations. Of course, V8 is not the only engine that can do this.

V8 also has a safeguard called de-optimization, which does the opposite, reversing the code into unoptimized code to prevent the engine from making incorrect guesses.

The garbage collection

For garbage collection, V8 uses a traditional generational tag sweep to clean up old generation data. The tagging phase prevents JavaScript from running. To control the cost of garbage collection and make JavaScript execution more stable, V8 uses incremental markup: instead of traversing the entire heap to mark every possible object, it only traverses part of the heap and then resumes normal execution. The next garbage collection starts where the previous iteration stopped, making the pauses between normal executions very short. As mentioned earlier, the cleanup operation is performed by a separate thread.

Ignition and TurboFan

With the release of V8 5.9 earlier in 2017, a new implementation pipeline was introduced. This new execution pipeline achieves greater performance gains and significant memory savings in real-world JavaScript applications.

The new execution pipeline is built on Ignition, the V8 interpreter, and TurboFan, the latest optimized compiler.

You can check out all of the V8 team’s blog posts on this topic here.

Since the release of V8 version 5.9, the V8 team has worked hard to keep up with JavaScript language features and optimizations for those features, Full-codegen and Crankshaft (which had been in V8’s service since 2010) were no longer used by V8 to run JavaScript.

This will mean a simpler, more maintainable architecture for V8 as a whole.

Improvements on the Web and Node.js

Of course these improvements are only the beginning. The new Ignition and TurboFan pipelines pave the way for further optimizations that will improve JavaScript performance and allow V8 to save even more resources in Chrome and Node.js for years to come.

Finally, here are some tips to help you write better, better JavaScript. I’m sure you can summarize these tips from above, but HERE are some of them for you:

How to write optimized JavaScript

  1. Order of object attributes: Be sure to use the same order when instantiating your object attributes so that hidden classes can be shared with subsequent optimization code.
  2. Dynamic properties: Adding properties after the object is instantiated forces the hidden class to change and slows down the execution of code optimized for the old hidden class. Therefore, all attributes are assigned in the object’s constructor.
  3. Method: Executing the same method repeatedly will run faster than executing a different method just once (because of inline caching).
  4. Arrays: Avoid sparse arrays with non-increasing keys, which are actually hash tables. Fetching each element in this array is expensive. Also, avoid applying for large arrays in advance. The best thing to do is grow the array slowly as you need it. Finally, do not delete elements in the array, as this will make keys sparse.
  5. Tagged values: V8 uses 32 bits to represent objects and numbers. It uses one bit to distinguish whether it is an object (flag = 1) or an integer (flag = 0), also known as a small integer (SMI) because it has only 31 bits. Then, if a number is greater than 31 bits, V8 boxes it, converts it to a double, and creates a new object to hold the number. So, to avoid costly box operations, try to use 31-bit signed numbers.

We at SessionStack try to follow these best practices to write high-quality, optimized code. The reason is that once you integrate SessionStack into your web application, it starts logging everything: all DOM changes, user interactions, JavaScript exceptions, stack tracking, network request failures, and debug messages. With SessionStack, you can treat problems in your web application like videos, and you can watch the replay to determine what happened to your users. None of this will affect your Web application. Here’s a free program to get you started.

More resources

  • Docs.google.com/document/u/…
  • Github.com/thlorenz/v8…
  • Code.google.com/p/v8/wiki/U…
  • Mrale. Ph/v8 / resource…
  • www.youtube.com/watch?v=UJP…
  • www.youtube.com/watch?v=hWh…

The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, React, front-end, back-end, product, design and other fields. If you want to see more high-quality translation, please continue to pay attention to the Project, official Weibo, Zhihu column.