“This article has participated in the weekend learning plan, click to see details”

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 compile JavaScript into bytecode in some form. Here is a list of popular projects that are implementing JavaScript engines:

  1. V8 – open source, developed by Google and written in C++
  2. Rhin O – Managed by the Mozilla Foundation, open source and developed entirely in Java
  3. SpiderMonkey – The first JavaScript engine that used to support Netscape Navigator and supports Firefox today
  4. JavaScriptCore – Open source, sold as Nitro, developed by Apple for Safari
  5. The KJS-KDE engine was originally developed by Harri Porten for the KDE project’s Konqueror Web browser
  6. Chakra (JScript9) — Internet Explorer
  7. Chakra (JavaScript) — Microsoft Edge
  8. Nashorn, open source as part of OpenJDK, is written by Oracle Java Languages and Tool Group
  9. JerryScript – is a lightweight Internet of Things engine.

Why build a V8 engine? The V8 engine built by Google is open source and written in C++. The engine 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 execution in Web browsers. For speed, V8 translates JavaScript code into more efficient machine code rather than using an interpreter. It compiles JavaScript code to machine code at execution by implementing a JIT (just-in-time) compiler, much as many modern JavaScript engines, such as SpiderMonkey or Rhino (Mozilla), do. The main difference here is that V8 does not generate bytecode or any intermediate code. V8 used to have two compilers before version 5.9 of V8 came out (released earlier this year), and the engine used two compilers: Full-CodeGen – a simple and very fast compiler that generates simple and relatively slow machine code. Crankshaft – a more complex (just-in-time) optimized compiler that generated highly optimized code. The V8 engine also uses several threads internally: the main thread does what you expect: Take your code, compile it and execute it and there’s a separate compile thread so that the main thread can continue to execute a Profiler thread while the main thread optimizes the code, which will tell the runtime what methods we’re spending a lot of time on, So that the crankshafts could optimize the few threads they were handling the garbage collector scan when the JavaScript code was first executed, V8 utilized full-CodeGen to convert the parsed JavaScript directly to the machine code without any conversion. This allows it to start executing machine code very quickly. Note that V8 does not use intermediate bytecode representations in this way, eliminating the need for an interpreter. After your code has been running for a while, the profiler thread has collected enough data to determine which method should be optimized. Next, crankshaft optimization starts in another thread. It converts the JavaScript abstract syntax tree into a high-level static singleton assignment (SSA) representation called Hydrogen, and attempts to optimize the Hydrogen diagram. Most optimizations are done at this level. The first optimization for inlining is to advance inlining as much code as possible. Inlining is the process of replacing the call point (the line of code calling the function) with the body of the called function. This simple step makes subsequent optimizations more meaningful.

Hidden classes

JavaScript is a prototype-based language: there are no classes, and objects are created using a cloning process. JavaScript is also a dynamic programming language, which means you can easily add or remove properties of 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 the value of an attribute in JavaScript more computationally expensive than retrieving it in a non-dynamic programming language such as Java or C#. In Java, all object properties are determined by a fixed object layout prior to compilation and cannot be added or removed dynamically at run time (well, C# has dynamic typing that’s another topic). Thus, property values (or Pointers to them) can be stored in memory as continuous buffers, with fixed offsets between each value. You can easily determine the length of the offset based on the property type, which is impossible in JavaScript because the property type can change at run time. Because using a dictionary to find the location of object attributes in memory is inefficient, V8 uses a different approach: hiding classes. Hidden classes work similarly 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); Once the “New Point(1, 2)” call occurs, V8 creates a hidden class named “C0”.

No attributes have been defined for Point, so “C0” is null. Once the first statement “this.x = x” is executed (inside the “Point” function), V8 creates a second hidden class named “C1” based on “C0”. “C1” describes where the property X can be found in memory (relative to the object pointer). In this case, “x” is stored at offset 0, which means that when a point object in memory is treated as a continuous buffer, the first offset will correspond to the attribute “x”. V8 will also update “C0” with “class transformation”, which indicates that if the attribute “x” is added to the point object, the hidden class should switch from “C0” to “C1”. The hidden class of the dot object below is now “C1”.

Each time a new property is added to an object, the old hidden class is updated to the transition path of the new hidden class. Hidden class conversions are important because they allow hidden classes to be shared between objects created in the same way. If two objects share a hidden class and the same attributes are added to them, the transformation ensures that both objects receive the same new hidden class and all optimized code. 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 named “C2” was created to add the class transformation to “C1”, stating that if attribute “y” is added to the Point object (which already contains attribute “X”), the hidden class should be changed to “C2” and the hidden class of the Point object updated to “C2”.

The hidden class conversion depends on the order in which attributes are added to the object. Take a look at the following code snippet:

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

For now, you assume that P1 and P2 will use the same hidden classes and transformations. Well, not really. For “P1”, first add attribute “A”, then attribute “b”. For “P2”, however, “B” is assigned first, followed by “A”. Thus, “P1” and “P2” end up with different hidden classes due to different transformation paths. In this case, it is best to initialize the dynamic properties in the same order so that the hidden classes can be reused.

Inline cache

V8 leverages another technique for optimizing dynamically typed languages, called inline caching. Inline caching 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 the general concept of inline caching (in case you don’t have time to read the in-depth explanation above).

So how does it work? V8 maintains a cache of object types passed as parameters in recent method calls and uses this information to make assumptions about object types that will be passed as parameters 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 stored information from previous look-up of object properties. Hidden classes.

So what does hidden classes have to do with the concept of inline caching? Whenever a method is called on a particular object, the V8 engine must look for that object’s hidden class to determine the offset to access a particular property. After two successful calls to the same method on the same hidden class, V8 omits the hidden class lookup 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 the hidden class has not changed and jumps directly to the memory address of the particular property using the offset from the previous lookup store. This greatly improves execution speed.

Inline caching is also why it is so 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 the previous example), V8 will not be able to use inline caching because even though the two objects are of the same type, their corresponding hidden classes assign different offsets to their attributes.

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

And then finally, Lithium gets compiled into machine code. Then something else happens called OSR: on-stack replacement. Before we start compiling and optimizing a method that obviously takes a long time to run, we’re probably running it. V8 won’t forget what it just did slowly to start over with an optimized version. Instead, it will transform all the contexts we have (stacks, registers) so that we can switch to the optimized version during execution. 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 unoptimized code if the assumptions made by the engine no longer hold.

For garbage collection, V8 uses the traditional generational approach of tagging and scavenging to clean up old ages. The markup phase should stop JavaScript execution. To control GC costs and make execution more stable, V8 uses incremental flags: Instead of traversing the entire heap, trying to mark every possible object, it traverses only a portion of the heap and then resumes normal execution. The next GC stop will continue where the previous heap traversal stopped. This allows for very short pauses during normal execution. As mentioned earlier, the scanning phase is handled by a separate thread.

With the release of V8 5.9 earlier in 2017, a new execution pipeline was introduced. This new pipeline enables greater performance improvements and significant memory savings in real-world JavaScript applications. The new execution pipeline builds on V8’s interpreter Ignition and TurboFan, V8’s latest optimized compiler.

Since V8 5.9 came out, V8 no longer executes JavaScript with full-CodeGen and Crankshaft (the technology that has served V8 since 2010), The V8 team has struggled to keep up with new JavaScript language features and the optimizations required for these features. This means that V8 as a whole will have a simpler, more maintainable architecture.

Improvements to The Web and Node.js benchmarks These improvements are just the beginning. The new Ignition and TurboFan pipelines pave the way for further optimizations that will improve JavaScript performance and reduce V8’s footprint in Chrome and Node.js over the next few years.

How to write optimized JavaScript

Order of object attributes: Always instantiate your object attributes in the same order so that you can share hidden classes and subsequently optimized code. Dynamic properties: Adding properties to an object after instantiation forces hidden class changes and slows down any methods optimized for previously hidden classes. Instead, all attributes of the object are allocated in its constructor. Methods: Code that executes the same method repeatedly runs faster than code that executes many different methods only once (due to inline caching). Arrays: Avoid sparse arrays whose keys are not incremental numbers. A sparse array without every element in it is a hash table. The cost of accessing elements in such arrays is higher. Also, try to avoid pre-allocating large arrays. It’s better to grow as you go. Finally, do not delete elements in the array. It makes the key sparse. Flag values: V8 uses 32 bits to represent objects and numbers. It uses a bit to know whether it is an object (flag = 1) or an Integer (flag = 0), and is called SMI (SMall Integer) because it has 31 bits. Then, if a number is greater than 31 bits, V8 boxes the number, converts it to a double, and creates a new object to put the number in. Use 31 signed digits to avoid expensive boxing of JS objects.