The originalHow JavaScript works: inside the V8 engine + 5 tips on how to write optimized code

A few weeks ago we started a series of blog posts aimed at digging deeper into JavaScript and figuring out how it works: We think that by understanding the building blocks of JavaScript and being familiar with how they fit together, it helps to write better code and applications.

The first article in this series focuses on providing an overview of the engine, runtime, and call stack. This article will take a closer look at the internal implementation of Google’s V8 engine. We’ll also offer some tips for writing better JavaScript code — the best practices our team follows when building SessionStack applications.

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 popular JavaScript engine projects:

  • V8– open source,GoogleThe development,C++write
  • RhinoMozillaFoundation managed, open source, fully usedJavaThe development of
  • SpiderMonkey– the firstJavaScriptEngine, formerly byNetscape NavigatorMaintenance, now byFirefoxmaintenance
  • JavaScriptCore— Open source, toNitroIn the name of sales byAppleThe company forSafariBrowser development
  • KJSKDEEngine, originally byHarri PortenKDEThe projectKonquerorBrowser development
  • Chakra(JScript9) –IEThe browser
  • Chakra– (JavaScript)EdgeThe browser
  • NashornOpenJDKPart of the open source project byOracle JavaAnd its toolset development
  • JerryScript – a lightweight Internet of Things engine

Why build a V8 engine?

Google’s V8 engine is an open source engine written in C++. The engine is used inside Google Chrome. But unlike the other engines, V8 is also used in the popular Node.js runtime.

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

V8 used to have two compilers

Prior to the V8 5.9 release (released in early 2017), the engine used two compilers:

  • Full-codegen — Simple, very fast compiler that generates simple and relatively slow machine code
  • Crankshafts – more complex (just-in-time) optimized compilers that generate highly optimized code

V8 also uses multiple threads internally:

  • The main thread does what you’d expect: it gets the code, compiles it, and then executes it
  • A separate thread is responsible for compiling so that the main thread can continue executing while the former optimizes the code
  • A parser thread tells the runtime which methods take a lot of time to useCrankshaftThe compiler optimizes the code
  • Several more threads handle garbage collection cleanup

When executing JavaScript code for the first time, V8 uses Full-CodeGen to translate parsed JavaScript directly into machine code without transition. This allows it to start executing machine code very quickly. Note that V8 does not use intermediate code representation, thus eliminating the need for an interpreter.

After your code has been running for a certain amount of time, the profiling thread can collect enough data to determine which methods need to be optimized.

Then the Crankshaft optimization started in another thread. It converts the JavaScript abstract syntax tree into an advanced static singleton assignment (SSA) representation, called Hydrogen, and attempts to optimize the nitrogen diagram. Most optimizations are done at this level.

inline

The first step in optimization is to inline as much code as possible. Inlining is the process of replacing a calling reference (the line of code on which a function is called) with the body of the called function. This simple step makes the rest of the optimization process more meaningful:

Hidden classes

JavaScript is a prototype-based language: there are no classes and objects are created by cloning. JavaScript is also a dynamic programming language, which means that objects can easily add and delete properties once they are initialized.

Most JavaScript interpreters use class dictionary data structures (based on hash functions) to store the in-memory location of object attribute values. This structure makes the computational overhead of retrieving property values in JavaScript more expensive than in non-dynamic languages such as Java or C#. In Java, all object attributes are determined by the fixed object layout prior to compilation and are not allowed to be dynamically added or removed at runtime (C# has dynamic typing, but that’s another topic). Thus, property values (or Pointers to properties) can be stored in memory as continuous buffers separated by fixed offsets. The length of the offset is simply determined based on the type of the property, however this is not possible in JavaScript because the property type can be changed at run time.

Since it is inefficient to find the location of an object’s attributes in memory through a dictionary, V8 uses an alternative: hiding classes. The principle of hidden classes is similar to 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

When the new Point(1, 2) call occurs, V8 creates a hidden class named C0.

Now Point has not defined any properties, so C0 is empty.

Once the first declaration this.x = x is executed (within the Point function), V8 creates a second c0-based hidden class C1. C1 describes where in memory (relative to the point object) the attribute X can be found. In this example, x is stored at offset 0, which means that when an object in memory is treated as a continuous buffer, the first offset corresponds to X. V8 will also update C0 with a “class conversion” to show that if an attribute X is added to a Point object, the hidden class C0 will be converted to C1. The hidden class of the point object below is now C1.

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

The same process is repeated when this.y = y is executed (again inside the Point function, after this.x = x).

A new hidden class C2 will be created, and a class conversion to C1 means that if you add attribute Y to a Point object (which already contains an attribute X), the hidden class should be updated to C2, and the hidden class of the Point object updated to C2.

Hidden class transformations depend on the order in which attributes are added to the object. Take a look at the following code snippet:

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 assume that P1 and P2 use the same hidden classes and transformations. This is not the case. For P1, add attribute A first and then attribute B. For P2, b is added first and then A. Therefore, P1 and P2 will end up with different hidden classes due to different transformation paths. In this case, it is best to keep the order consistent when initializing dynamic properties so that the same hidden classes can be reused.

Inline cache

V8 leverages another technique called inline caching to optimize dynamically typed languages. Inline caching relies on the observation that repeated calls to the same method usually occur on objects of the same type. An in-depth look at inline caching is provided here.

We’re going to cover the general concept of inline caching (in case you don’t have time to check out the in-depth explanation above).

So how does it work? V8 maintains a cache of object types passed in as arguments in recent method calls and uses this information to make assumptions about the types of objects that will be taken as arguments in the future. If V8 does a good job of assuming the type of object the method is passing in, it can skip the calculation of how to get the object’s attributes and instead use the information stored when looking up the object’s hidden class.

So how do hidden classes relate to inline caches? Whenever an object calls a method, V8 must perform a query of the object’s hidden class to determine the offset to access an attribute. When the same method has been successfully called twice on the same hidden class, V8 omits the query on the hidden class and only adds the property offset to the object pointer itself. For all future calls to that method, V8 assumes that the hidden class remains unchanged and jumps directly to the memory address of an attribute using the offset stored by the previous query. This greatly improves execution speed.

Inline caching is also why it is so important for homogeneous objects to share the same hidden class. If you create two identical objects with different hidden classes (as in the previous example), V8 cannot use inline caching because even though the two objects are of the same type, their corresponding hidden classes specify different offsets for attributes.

The two objects are basically the same, but the a and B attributes are created in different order.

Compile to machine code

Once the nitrogen plot was optimized, the Crankshaft would reduce it to a lower level representation called Lithium. Most implementations of Lithium depend on specific architectures. Register allocation occurs at this level.

Ultimately, Lithium is compiled into machine code. This is followed by OSR: on-stack substitution. We might run it before we start compiling and optimizing the apparently long-running method. V8 will not forget the slow execution when it starts implementing the optimized version again. Instead, we convert all of our contexts (stacks, registers) so that we can switch to the optimized version during execution. This is a very complex task, and remember that V8 inlined code first among other optimizations. The V8 is not the only engine capable of this.

There is also a safety measure called de-optimization that does the reverse, falling back to unoptimized code in case the assumptions made by the engine no longer hold.

The garbage collection

In terms of garbage collection, V8 uses the traditional generational approach of marking and sweeping to clean up old generations. The tagging phase pauses JavaScript execution. To control the overhead of garbage collection and make execution more stable, V8 uses incremental markup: instead of traversing the entire stack heap, it tries to mark every possible object, traverses only a portion of the stack heap, and then resumes normal execution. The next garbage collection pause continues where the stack heap stopped. This allows for fairly short pauses during normal implementation. As mentioned earlier, the cleanup phase is handled by a separate thread.

Ignition and TurboFan

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

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

You can check out the V8 team’s blog post on the subject here.

Since the release of V8 5.9, V8 has not used full-CodeGen and crankshafts (which had been powering V8’s technology since 2010) in JavaScript execution, This is because the V8 team is also struggling to keep up with new JavaScript language features and the optimizations they require.

This means that V8 will have a much simpler and more maintainable architecture overall.

These improvements are just the beginning. The new Ignition and TurboFan pipelines pave the way for further optimization that will boost JavaScript performance and shrink V8’s footprint in Chrome and Node.js for years to come.

Finally, here are a few tips and tricks on how to write better, optimized JavaScript code. Although you can easily get these from the above, they have been summarized as follows for convenience:

How to write optimized JavaScript

  1. Order of object attributes: Always initialize object attributes in the same order so that hidden classes and subsequent optimization code are shared.
  2. Dynamic properties: Adding object dynamic properties after initialization forces changes to the hidden class and slows down previously optimized methods of the hidden class. Instead, specify all attributes in the object’s constructor.
  3. Methods: Code that executes the same method repeatedly will run faster than many different methods executed only once (due to inline caching).
  4. Arrays: Avoid sparse arrays with non-incrementing keys. A sparse array where not every element exists is a hash table. Accessing elements of a sparse array will be more expensive. Also, avoid pre-allocating large arrays. It is best to increase the length as needed. Finally, do not delete elements in the array. This will make the array sparse.
  5. Tagged value:V8Objects and numbers are represented by 32-bit bytes. A bit is used to identify either an object (identified as 1) or an integer (identified as 0), called because they are 31 bitsSMI(SMall Integer). If a number is larger than 31 digits that can be represented,V8It will wrap it, convert it to a double-byte value and create a new object to store it in. Avoid using the signed value of 31JSExpensive wrapper operations for objects.