An Overview of the Engine, the Runtime, and the Call Stack by Alexander Zlatkov

A few weeks ago we started a series of documents aimed at getting a deeper understanding of JavaScript and how it works: we think you can write better code and applications by understanding the components of JavaScript and how they all work together.

The first installment in this series provides an overview of the engine, runtime, and call stack. The second installment takes a closer look at the V8 engine. We’ll also provide some quick tips on how to write better JavaScript code, which is the best practice our SessionStack (the author’s company) development team follows when building 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 as just-in-time compilers that compile JavaScript into some form of bytecode.

The following list is a list of popular projects that implement JavaScript engines

  • V8 – open source, developed by Chrome and written in C++
  • Rhino – Managed by the Mozilla Foundation, open source and written in Java
  • SpiderMonkey – The first JavaScript engine that used to support Netscape Navigator and now supports FireFox
  • JavaScriptCore – Open source, sold by Nitro and developed by Apple for Safari
  • KSJ — Harde Porten originally developed the KDE engine for the KDE project’s Konqueror Web browser
  • Available (JScript9) – Internet Explore
  • CHakra(JavaScript) — Microsoft Edge (New version of chromium-based, that is, JavaScript engine using V8)
  • Nashorn — Partially open source by OpenJDK, written by the Oracle Java and Tool group
  • JerryScript — Lightweight engine for the Internet of Things

Why V8?

V8 engine is an open source project built by Google and written in C++. The engine is used inside Chrome. Unlike other engines, V8 is also used in the popular Node.js.

Two compilers used by V8

Before V8’s 5.9 release, the engine used two compilers: TurboFan, TurboFan, TurboFan and TurboFan.

  • Full-codegen – a simple but fast compiler that produces simple and relatively slow machine code
  • GrankShaft – a complex (just-in-time) optimized compiler that produces highly optimized code

The V8 engine uses multithreading internally:

  • The main thread does what you expect it to do: pull your code, compile it, and execute it
  • In addition, a separate thread is used for compilation, so the main thread can continue executing while the thread is optimizing the code
  • Profiler threads can tell the runtime which methods will take a lot of time so that Grankshaft can optimize them
  • Some cleanup worker threads that handle the GarBage Collector are optimizing the code

When executing JavaScript code for the first time, V8’s Full-CodeGen escapes the parse JavaScript code as machine code. This allows it to execute machine code quickly. Note that V8 does not use intermediate bytecode representations in this way, thereby eliminating the need for an interpreter.

When your code has been executing for a while, the profiler thread has gathered enough data to determine which method needs to be optimized

Next, Grankshaft starts tuning in another thread. It converts the JavaScript abstract syntax tree into an advanced static single assignment (SSA) representation called Hydrogen, and attempts to optimize the Hydrogen diagram. Most optimizations are done at this level.

Photo Address:V8. Dev/blog/igniti…

3. Inline

The first optimization is to inline as much code in advance as possible. Inlining is the process of replacing the calling part (the line of code calling the function) with the body of the called function. This simple step makes subsequent optimizations more meaningful.

4. Hidden classes

JavaScript is a prototype-based language: there are no classes and objects created using clones. JavaScript is also a dynamic programming language, which means you can easily add or remove properties after you instantiate an object.

Most JavaScript interpreters use the structure of a class dictionary (based on hash functions) to store the locations of object attribute values in memory. Retrieving the value of a property using this structure is computationally more expensive in JavaScript than in non-dynamic programming languages such as Java or C#. In Java, all object properties are determined by a fixed object structure prior to compilation, and cannot be added or removed dynamically at run time (C# has dynamic typing, which is another topic). As a result, the values of attributes (or Pointers to them) can be stored in memory as continuous buffers, with fixed offsets between each buffer. You can easily determine the length of the offset based on the attribute type, but this is not possible in JavaScript because JavaScript can change the attribute type at run time.

Since it is inefficient to use dictionaries to find the location of object attributes in memory, V8 uses another approach: hiding classes. Hidden classes work similar to the fixed object structures (classes) used in languages like Java, but they are created at run time. Now, let’s see what they look like in action:

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 executed, V8 will create a hidden class called “C0”

There are no attributes on class C0 yet, so “C0” is empty.

The first statement “this.x = x” is executed (inside the Point function), and V8 will create a second hidden class “C1” based on “C0”. “C1” describes the memory location (equivalent to an object pointer) where x attributes can be found. In this example, the offset “x” can store is 0, which means that the Point object in memory can be treated as a continuous buffer, with the first offset corresponding to “x”. If the attribute “x” is added to the object “point”, V8 will also update “C0” through the “Class Transition”, converting the hidden class from “C0” to “C1”. Now the hidden class is “C1”.

Each time a new property is added to the object, the old hidden class is converted 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. If two objects share a hidden class and the same attributes are added to both of them, the class conversion ensures that both objects receive the same new hidden class and subsequent optimization code.

When the statement “this.y = y” (inside the Point function, after this.x=x) is executed, the appeal step is repeated.

A new hidden class “C2” is created, and the “class transform” will be applied to “C1”, where the attribute “y” is added to the Point object (which already contains attribute X). Hidden classes are converted from “C1” to “C2”.

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 assume that P1 and P2 will apply the same hidden classes and class conversions. However, this is not true. For P1, attribute A is added first and then attribute B is added, but for P2, attribute B is added first and then attribute A is added. Thus, P1 and P2 are different hidden classes and different class conversions. In this case, it is best to initialize the dynamic properties in the same order to reuse the hidden classes.

5. Inline cache

V8 takes full advantage of another technique for optimizing dynamic languages called inline caching. Inline caching relies on the fact that calls to the same method tend to occur on objects of the same type.

See here for an in-depth explanation of inline caching.

We’ll cover the general concept of inline caching (in case you don’t have time for the above in-depth explanation)

How does it work? V8 maintains a cache of object types passed as arguments in the most recent method call. Use this information to make assumptions about the type of object that will be passed as a parameter. If V8 can make good assumptions about the type of object that will be passed to the method in the future, it can bypass the process of figuring out how to access the object’s properties and instead use the stored information of the previously found object’s hidden class, which can access the properties via offsets.

How are hidden classes and inline caches related? When a method is called on an object, the V8 engine queries the object’s hidden class to determine whether to use offsets to access the object’s properties. If the same method is called twice to the same hidden class. After successfully calling the same method twice 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 subsequent calls using 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 stored in the previous lookup. This greatly improves execution speed.

Example (note: because the author is vague in this paragraph, he added an example by referring to other articles, which is not in the original article) :

function getX(o) {
	return o.x;
}
Copy the code

If it is JSC (JavaScriptCore is a JS engine like V8, see above), the bytecode is generated.

get_by_id

The JSC also embeds the inline cache in the get_by_id directive, which consists of two uninitialized slots. Shape is the same as the inline class mentioned above, except that it is expressed in a different way. It belongs to SpiderMonkey.

Now suppose we call getX with the object {x: ‘a’}. Shape for this object has the property “x”, and Shape stores offsets and properties for that property. The first time this function is executed, the get_by_id directive looks for attribute “x” and finds that the value is stored at offset 0.

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 but have different hidden classes (as we mentioned earlier). Even if two classes have the same type, V8 will not use inline caching because their hidden classes assign different offsets to the same attribute.

Compile to machine code

Once the Hydrogen diagram was optimized, the Crankshaft reduced it to a lower-level representation called Lithium. Most implementations of Lithium are structure-specific. Register allocation also occurs at this level.

Finally, Lithium compiles to machine code. This is then called on-stack replacement for the start of OSR. We run it first while compiling and optimizing the apparently long method. V8 does not forget that the execution of optimized code is slow when it is restarted. So, it will convert all of our contexts (heap, registers) so that we can switch to the optimized version during execution. This is a very complex task, and keep in mind that, of all the optimizations, V8 has already inlined the code at the beginning. V8 is not the only engine that can do this.

There is a safeguard called de-optimization that allows you to reverse the transformation and revert to unoptimized code if the engine is no longer available.

Seven. Recycling

For garbage collection, V8 uses the traditional generational approach of token cleanup to clean up old generation objects. 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, a portion of the heap is traversed, and then normal execution resumes. The next GC picks up where the previous heap traversal stopped. This allows for very short pauses during normal execution. As mentioned earlier, the cleanup phase is handled by a separate thread.

B: Ignition and TurboFan

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

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

You can check it out in the V8 team article.

Full-codegen and Crankshaft (the technology that developed service V8 since 2010) are not used for Javascript execution in V8 since V8 5.9 was released. The V8 team has struggled to keep up with new JavaScript language features and the optimizations they require.

This means that V8 as a whole will have a simpler and maintainable architecture in the future.

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.

Finally, here are some tips on how to write optimized JavaScript. You can easily get them from above, but for your convenience, here’s the summary:

  1. Define an order for object properties: Always instantiate object properties in the same order so that hidden classes and subsequent optimized code can be shared
  2. Dynamic Properties: Adding properties to an object after it has been instantiated forces changes to the hidden class and slows down the execution of all methods optimized for the previously hidden class. It is best to assign all attributes of an object in its constructor.
  3. Methods (inline caching) : Executing the same code repeatedly is much faster than executing different code once (due to inline caching).
  4. Arrays: Avoid sparse arrays whose keys are not incremental numbers. A sparse array with no elements filled is a hash table. The elements in this array are more expensive to access. Also, try to avoid pre-allocating large arrays. It’s best to grow with your needs. Finally, do not delete elements in the array. It makes the keys sparse.
  5. Tagged 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 called SMI (small Integer) (flag= 0), since it has 31 bits. Then, if the number is greater than 31 bits, V8 wraps the number, makes it a double and creates a new object to put the number in. Use 31 signed digits whenever possible to avoid expensive encapsulation of JS objects.

Other articles in this series

  1. An overview of the engine, runtime, and call stack
  2. Learn more about the V8 engine & 5 tips on How to Write Optimal code
  3. Memory Management & How to deal with 4 common Memory leaks
  4. Event loops and the rise of asynchronous programming & 5 ways to Code better with Async /await
  5. Learn more about WebSockets and HTTP2 with SSE & How to choose the right path
  6. Compare WebAssembly & Why is it better than JavaScript in some cases
  7. The construction of Web Workers & 5 cases where you need to use it
  8. Service Workers, its lifecycle and use cases
  9. Web Push Notifications mechanism
  10. Track DOM changes through MutatioinObserver
  11. Rendering engine and optimization tips
  12. In-depth understanding of the network layer & performance optimization and security
  13. Understand the inner workings of CSS and JS animation & performance optimization
  14. Parsing, Abstract Syntax tree (ASTs) & How to optimize parsing time
  15. Internals of Classes and Inheritance & Babel and TypeScript Escape
  16. Storage Engine & How to choose the right Storage API
  17. The inner workings of Shadow Dom & How to build independent components
  18. WebRTC and peer-to-peer mechanisms