A few weeks ago, we started a series of articles that delve into JavaScript and how it actually works, and we think that by understanding the building blocks of JavaScript and how they work together, you’ll be able to write better code and applications.

The first article in this series focused on an overview of the engine, runtime, and call stack. The second article will delve into the guts of Google’s V8 JavaScript engine.

An overview of

A JavaScript engine is a program or interpreter that executes JavaScript code. A JavaScript engine can be understood as a standard interpreter, or a runtime compiler, which compiles JavaScript into bytecode in some form.

  • V8Open source software developed by Google, written in C ++
  • RhinoManaged by the Mozilla Foundation, it is open source and developed entirely in Java
  • SpiderMonkeyThe first JavaScript engine to support Netscape Navigator, now supports Firefox

  • JavaScriptCore-Open source, sold as Nitro, developed by Apple for Safari

  • Kjs-kde engine, originally developed by Harri Porten for the KDE project’s Konqueror Web browser

  • Chakra (JScript9)-
    Internet Explorer
  • Chakra (JavaScript)- Microsoft Edge

  • Nashorn-Java language and tool set by OracleOpen source as part of OpenJDK

  • JerryScript— a lightweight engine for the Internet of Things

Why was V8 created?

The V8 engine is an open source program written and built by Google in C ++. It is used in Google Chrome. However, unlike other engines, V8 is also used in the popular Node.js runtime.



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

V8 used to have two compilers

Prior to V8 version 5.9 (released earlier this year), the engine used two compilers:

  • Full-codegen — a simple and fast compiler that generates simple but unoptimized machine code.
  • Crankshaft – a more sophisticated (just-in-time) optimization compiler that generates highly optimized code.

Multiple threads are also used in V8:

  • Main thread: Gets the code, compiles the code, and executes it

  • There is also a separate thread that is used to compile, so the main thread can continue to execute while it optimizes the code
  • An analysis thread that tells the runtime which methods are consuming a lot of time so that
    CrankshaftYou can optimize them
  • Some threads handle the scanning garbage collector

When the JavaScript code is first executed, V8 leverages full-CodeGen to convert the parsed JavaScript directly into machine code without any conversion in the middle of the process. This allows it to start executing machine code very quickly. Note that V8 does not use intermediate bytecode representation, so no interpreter is required.

After your code has been running for a while, the profiling thread has collected enough data to tell you which method to optimize.

Next, the Crankshaft optimization started with another thread that turned the JavaScript abstract syntax tree into an advanced static singleton assignment (SSA) representation named Hydrogen and attempted to optimize the Hydrogen chart, where most of the optimizations were done.

inline

The first optimization is to embed as much code as possible ahead of time. Embedding is the process of the called function replacing the calling method (the line of code calling the function). This simple step makes subsequent optimizations more meaningful.



Hidden classes

JavaScript is a prototype-based language, it does not create classes, objects are created by reference, and JavaScript is also a dynamic programming language, which means you can easily add or remove properties from objects after instantiation.

Most JavaScript parsers use dictionary-like structures (based on hash functions) to store the in-memory location of object attribute values. This structure makes retrieving property values in JavaScript more computationally expensive than in non-dynamic programming languages such as Java or C#. In Java, All object properties are determined by a fixed object template before compilation, and unable to dynamically add or delete at runtime (c # dynamic type, this is another topic), as a result, the attribute value (or pointer to these attributes) can be used as a buffer in memory in a row, has a fixed offset between each buffer, The length of the offset can be easily determined based on the attribute type. This is not possible in JavaScript, where property values can be changed at run time.

Since it is inefficient to use dictionary structures to locate attribute values in memory, V8 uses a different approach instead: hiding classes. Hidden Classes act like fixed object templates in the Java language, unless they are created at run time. 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 theNew Point (1, 2)The call occurs, and V8 creates a hidden class named “C0”.

No attributes have been defined for Point, so “C0” is null.

Once the first line of this.x = x is executed (inside the Point method), V8 will create a second hidden class “C1” based on “C0”. “C1” describes where in memory the attribute x can be found (relative to the object pointer). This means that when a Point object in memory is treated as a continuous buffer, the first offset position will correspond to the attribute “X”. V8 will also update “C0” with “class conversion”, which means that if attribute “x” is added to a Point object, the hidden class should switch from “C0” 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 the transformation path pointing to the new hidden class. Hidden class conversions are important because they allow hidden classes to be shared between objects created in the same way (such as instantiating two Point objects whose common hidden class is C0). If two objects share a hidden class and the same attribute is added to them, the transformation will ensure that both objects receive the same new hidden class (such as adding an "X" attribute, which will both point to C1) and all optimization code

This process is repeated when “this.y=y” is executed (” this.y=y “in the Point function). If the attribute” y “is added to the Point, the class conversion will generate” C2 “hidden classes based on” C1 “, and the hidden classes of the Point object will be updated to “C2”.



Hidden class conversions depend on the order in which attributes are added to the object. Look at the following code:

functionPoint(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, assume that the same hidden classes and transformations will be used for P1 and P2. Well, not really. For “P1”, first add attribute “A”, then attribute “b”. However, for “P2”, “b” is assigned first, followed by “A”. Thus, “P1” and “P2” end up with different hidden classes and different class conversions. In this case, it is much better (recommended) to initialize the dynamic properties in the same order so that the hidden classes can be reused.

Inline cache

Another way V8 optimizes the dynamically typed language is called inline caching, which relies on the observation that repeated calls to the same method tend to occur on objects of the same type. An in-depth explanation of inline caching can be found here.

We’ll discuss some of the concepts of inline caching (if 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 as arguments in recent function method calls and uses this information to make assumptions about object types passed as arguments in the future.
If V8 can make good assumptions about the type of object that will be passed to the method, it can bypass the process of determining how to access object properties and instead use the stored information of the hidden class used by the previously found object.

What about the concept of how hidden classes and inline caching are related?

Whenever a method is called on a particular object, the V8 engine must perform a lookup of that object’s hidden class to determine the offset to access a particular property.
After successfully calling the same method twice to the same hidden class, V8 omits the lookup of the hidden class and simply adds the offset of the property to the object pointer itself.
For all future calls to this method, the V8 engine assumes that its hidden class has not changed and jumps directly to the memory address of the particular property using the offset stored in the previous lookup. This greatly improves execution speed.

Inline caching is also why it is important for objects of the same type to share hidden classes.

If you create two objects of the same type and different hidden classes (as we did in our previous example), V8 will not be able to use inline caching because even if the two objects are of the same type, their corresponding hidden classes will assign different offsets to their attributes.




The two objects are essentially the same, but the "A" and "b" properties are created in different order.

 

Compile to machine code

Once the Hydrogen chart was optimized, the Crankshaft reduced it to a low level representation called Lithium.
Most Lithium implementations rely on the overall architecture. Register allocation occurs at this level.

Finally, Lithium is compiled into machine code. Then something called OSR happened: Stack replacement (OSR).
When we start compiling and optimizing an obviously time-consuming method, we’ve probably been running it all along. V8 does not throw away its previously slow code and re-execute the optimized code. Instead, he does a conversion of all the contexts (stacks, registers) that the slow code has so that he can switch directly to the optimized version while executing the slow code.


This is a very complex task, keep in mind that, among other optimizations, V8 initially inlined the code. V8 is not the only engine that can do this.

A safeguard called de-tuning can do the opposite and revert to non-optimized code if the assumptions made by the engine no longer apply.

The garbage collection

For garbage collection, V8 uses the traditional generational tag removal garbage collection mechanism to remove the older generation. JavaScript stops executing during the tag phase. In order to control the cost of GARBAGE collection and the stability of code execution, V8 uses incremental tags: And traverse the entire heap, trying to mark every possible object is different, it’s just part of the tag heap, and then resume normal execution, the next time the GC will continue to traverse, from the place where the last stop in the execution time periods, it allows a short pause, as previously mentioned, the sweep phase in a separate thread.



With the release of version 5.9 of V8 earlier in 2017, a new execution pipeline was introduced that achieved even greater performance gains and significant memory savings in actual JavaScript referencing programs.

The new pipeline is built on TOP of 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 version 5.9 of V8, as the V8 team struggled to keep up with the new JavaScript language features and the optimizations required for them, Full-codegen and Crankshaft (which have been serving V8 since 2010) are no longer used by V8 to run JavaScript.

This means that V8 as a whole will have a simpler and more maintainable architecture.



Improvements to the Web and Node.js benchmarks

These optimizations are just the beginning, and the new Ignition and TurboFan pipelines pave the way for future optimizations that will allow even greater improvements in JavaScript performance and allow V8 to conserve resources in Chrome and Node.js.

Finally, here are some tips to help you write better, higher-quality JavaScript. You can certainly draw some tips easily from the above, but a summary is provided for your convenience.

How to write the best JavaScript code

1. Order of object properties: Always instantiate your object properties in the same order, so that hidden classes and subsequent optimization code can be shared.

2. Dynamic properties: Adding new properties to an object after its instantiation causes hidden class changes that slow down the execution of methods optimized for the old hidden class. Therefore, try to allocate all the attributes of the object in the constructor.

3. Methods: Code that executes the same method repeatedly will run faster than code that executes a different method only once (due to inline caching).

4. Arrays: Avoid Sparse Arrays with keys that aren’t increasing numbers. A sparse array that does not allocate memory for each element is essentially a hash table. The elements in this array are more expensive to fetch than the elements in a normal array. Also, avoid using large arrays of pre-requisition. It is best to increase the size of the array slowly as needed. Finally, do not delete elements in the array, as this will make keys sparse.

5. Token values: V8 uses 32 bits to represent objects and numbers. It uses one bit to distinguish between an object (flag = 1) and an Integer (flag = 0) (called SMI or SMall Integer because it has only 31 bits to represent the value). Then, if a number is greater than 31 bits, V8 boxing the number, turning it into a double, and creating a new object to put the double into. So, to avoid the costly Boxing operation, use a 31-bit signed number.

Follow-up document translation will follow up!!

Welcome to the xuan xuan front public account, the follow-up will launch a series of articles “a large graphical application 0 to 1 process”, this account will also be updated synchronously