May 31, 2021 Article blog
This article comes from the public number: CoyPan, co-author of "CoyPan" in line with expectations
Original title: A Deep Dive Into V8
Original link: blog.appsignal.com/2020/07/01/a-deep-dive-into-v8.html?utm_source=javascript-weekly-sponsored&utm_medium=email&utm_campaign=deep-dive-v8&utm_content=sponsored-link
Most front-end developers come across a buzzword:
V8
It's popular in large part because it takes JavaScript's performance to the next level.
Yes,
V8
is fast. B
ut how does it play its magic?
Why is it responding so quickly?
"The
V8
is Google's open source high-performance
JavaScript
and
WebAssembly
engine, written in
C++
the official documents state.
It's mainly used in
Chrome
and
Node.js
and so on.
In other words,
V8
is software developed by
C++
that compiles
JavaScript
into executable code, the machine code.
Now, let's start to see more clearly that
Chrome
and
Node.js
just a bridge to deliver JS code to its final destination: machine code running on a particular machine.
Another important role in
V8
performance is its generational and ultra-accurate garbage collector.
It is optimized to collect objects that JavaScript no longer
needs
with low memory.
In addition,
V8
relies on a set of other tools and features to improve some of JS's inherent functionality.
These features tend to slow down JS (such as the dynamic nature of JS).
In this article, we'll explore these tools (Ignition and TurboFan) and features in more detail.
In addition, we'll cover the basics of
V8
internal capabilities, compilation and garbage collection processes, and single-threaded features.
How does the machine code work? Simply put, machine code is a very low-level set of instructions that are executed in a specific part of machine memory.
The process of
C++
a machine code, for example, is like this:
Before going any further, it is important to note that this is a compilation process that differs from the JavaScript interpretation process. In fact, the compiler generates a complete program at the end of the process, and the interpreter works as a program itself, completing tasks by reading instructions (usually scripts, such as JavaScript scripts) and converting them into executable commands.
The interpretation process can be dynamic (the interpreter parses and only runs the current command) or fully parsed (that is, the interpreter translates the script completely before proceeding with the appropriate machine instructions).
Back in the diagram, the compilation process usually starts with the source code. Y ou implement the code, save it, and run it. T he running process starts with the compiler in turn. T he compiler is a program that, like other programs, runs on your machine. I t then traverses all the code and generates the object file. T hose files are machine codes. They are optimization code that runs on a specific machine, which is why you must use a specific compiler when you move from one operating system to another.
But you can't execute individual object files, you need to combine them into a single file, the well-known
.exe
file (executable).
This is
Linker
job.
Finally,
Loader
is the agent responsible for transferring code from the exe file to the virtual memory of the operating system. I
t is basically a means of transport.
Here, your program finally starts running.
Sounds like a long process, doesn't it?
Most of the time (unless you're a developer using assembly on a bank mainframe), you'll spend time programming in high-level languages: Java, C#, Ruby, JavaScript, and so on.
The higher the language, the slower it is.
That's why
C
and
C++
faster because they're very close to the machine code language: assembly language.
In addition to performance, one of the main advantages of
V8
is the possibility of exceeding
ECMAScript
standard and understanding
C++
JavaScript
is limited to
ECMAScript
The
V8
engine, in order to exist, must be compatible, but not limited to
JavaScript.
It's great to have the ability to integrate the features of
C++
into
V8
Because
C++
special nature of file processing and memory/threading that has evolved to very good OS operations, it is useful to have all of these capabilities in
JavaScript.
If you think about it,
Node.js
itself was born in a similar way.
It follows a path similar to
V8
plus server and network functionality.
If you are a
Node
developer, you should be familiar with the single-threaded nature of
V8
A JS execution context is proportional to the number of threads.
Of course,
V8
manages the operating system threading mechanism in the background.
It can work with multiple threads because it is a complex software that can perform many tasks at the same time.
However,
V8
creates only a single-threaded environment for each
JavaScript
execution context.
The rest is under
V8
control.
Imagine the stack of function calls that JavaScript code should make. ript works by stacking one function on top of another, following the order in which each function is inserted/called. W e can't know if it calls other functions until we reach the contents of each function. If this happens, the called function is placed behind the caller in the stack.
For example, when callbacks are involved, they are placed at the end of the stack.
Managing the memory required for this stack organization and process is one of
V8
main tasks.
Since release 5.9 in May 2017,
V8
has come with a new
JavaScript
execution pipeline built on top of
V8
interpreter
Ignition
It also includes an update and a better optimization compiler
-TurboFan
These changes are entirely focused on overall performance and the difficulties Google developers face in adapting their engines to all the rapid and significant changes in the JavaScript space.
From the beginning of the project,
V8
maintainers have been worried about finding a good way to improve
V8
performance while
JavaScript
continues to evolve.
Now that we can see the Benchmarks test
Benchmarks
for the new engine, there's been a huge improvement:
This is another magic trick for V8.
ript
is a dynamic language. T
his means that new properties can be added, replaced, and deleted during execution.
For example, in a language like Java, this is not possible,
Java
everything (classes, methods, objects, and variables) must be defined before the program executes and cannot be changed dynamically after the application starts.
Because of its special nature, JavaScript interpreters typically perform dictionary lookups based on hash functions (hash algorithms) to know exactly where this variable or object is allocated in memory.
This is expensive for the last process. I n other languages, when objects are created, they receive an address (pointer) as one of their implicit properties. This allows us to know exactly where they are in memory and how much space to allocate.
For
JavaScript,
this is not possible because we cannot map content that does not exist.
That's where
Hidden Classes
comes into play.
Hidden classes are almost identical to classes in
Java
static and fixed classes have unique addresses to locate them.
However,
V8
is not executed before the program executes, but each time the object structure changes "dynamically" during the run.
Let's look at an example to illustrate the problem. Consider the following snippets of code:
function User(name, fone, address) {
this.name = name
this.phone = phone
this.address = address
}
In the prototype-based feature of JavaScript, each time a new user object is instantiated, assume:
var user = new User("John May", "+1 (555) 555-1234", "123 3rd Ave")
V8
then creates a new hidden class.
We call it
_User0
Each object has a reference in memory to its class representation. I t is a class pointer. A t this point, because we've just instantiated a new object, we've created only one hidden class in memory. It's empty now.
When you execute the first line of code in this function, a new hidden class is created on top of the previous one, this time
_User1
It is basically the memory address of
User
with the
name
property.
In our example, we didn't use
user
name
only as a property, but each time we do this, this is the hidden class that
V8
will load as a reference.
name
property is added to the offset of the memory buffer by 0, which means that this is considered the first property in the final order.
V8
also adds a conversion value to
_User0
hidden class.
This helps the interpreter understand that each time you add a name property to a User object, you must handle the conversion from
_User0
to
_User1
When the second line in the function is called, the same procedure occurs again and a new hidden class is created:
You can see the hidden class trace stack. In a chain maintained by a converted value, one hidden class leads to the other.
The order in which properties are added determines how many hidden classes
V8
will create. I
f you change the order of the lines in the snippets we create, different hidden classes will also be created.
That's why some developers try to keep the order in which hidden classes are reused, reducing overhead.
This is a very common term in the JUST-in-Time compiler. It is directly related to the concept of hidden classes.
For example, whenever you call a function and pass an object as an argument, V8 sees the action and thinks, "Well, this object was successfully passed two or more times as an argument... W hy not store it in my cache for future calls instead of performing the entire time-consuming hidden class validation process again? ”
Let's review the previous example:
function User(name, fone, address) { // Hidden class _User0
this.name = name // Hidden class _User1
this.phone = phone // Hidden class _User2
this.address = address // Hidden class _User3
}
When we pass an instance of the User object to the function twice as an argument,
V8
jumps to the hidden class lookup and goes directly to the property of the offset.
This is much faster.
However, keep in mind that changing the order in which any property assignments in a function results in different hidden classes, so
V8
will not be able to use inline caching.
This is a good example of how developers should not avoid gaining a deeper understanding of the engine. Instead, having this knowledge will help your code execute better.
Do you remember when we mentioned that
V8
collects memory garbage in another thread?
This is helpful because our program execution is not affected.
V8 uses a well-known "tag and scan" policy to collect old objects in memory. In this strategy, the stage at which GC scans memory objects to "tag" them for collection is a bit slow because it requires pausing code execution.
However, V8 is incremental, which means that V8 tries to mark as many objects as possible for each GC pause. I t makes everything faster because you don't need to stop the entire execution until the collection is finished. In large applications, performance improvements vary greatly.
That's what
W3Cschool编程狮
has learned about
JavaScript V8,
and I hope it will help you.