深刻讨论 V8

v8-weixin.png

原文做者:Diogo Souza

原文连接:https://blog.appsignal.com/20...javascript

大多数前端开发人员一直在讨论这个时髦词:V8。它的流行很大程度上是由于它将 JavaScript 的性能提高到了一个新的水平。html

是的,V8很是快。 可是,它是如何发挥其魔力的,为何它会如此敏捷?前端

官方文档指出,“ V8是用 C ++ 编写的Google的开源高性能JavaScript和WebAssembly引擎。 它用于Chrome 和 Node.js 等。”java

换句话说,V8是一个用c++开发的软件,能够将JavaScript转换成可执行机器代码。c++

Google Chrome和Node.js都只是将JavaScript代码传输到其最终目的地的桥梁:在该特定机器上运行的机器代码。编程

V8性能表现中的另外一个重要角色是它的分代和超级精确的垃圾收集器。 它通过优化,收集JavaScript再也不须要的对象,使得占用内存低。缓存

除此以外,V8还依靠其余工具和功能来改善某些固有的 JavaScript 功能,这些功能在过去会使语言变慢(例如,它的动态特性)。服务器

在本文中,咱们将更详细地探讨这些工具(Ignition 和 TurboFan)及其功能。 除此以外,咱们还将介绍V8的内部功能,编译和垃圾回收过程,单线程性质等基础知识。网络

一、从基础开始

机器代码如何工做? 简而言之,机器代码是一堆很是低级的指令,它们在机器内存的特定部分中执行。app

使用 c++ 语言做为参考生成它与它相似的功能:

figure01.png

在进一步讨论以前,必须指出这是一个编译过程,它与 JavaScript 解释过程不一样。实际上,编译器在过程结束时生成整个程序,而解释器做为程序自己来工做,它经过读取指令(一般做为脚本,好比JavaScript脚本)并将它们翻译成可执行命令来完成这项工做。

解释过程能够是动态的(解释器解析并只运行当前命令),也能够是彻底解析的(即解释器在继续执行各自的机器指令以前首先彻底翻译脚本)。

回到图中,正如所见,编译过程一般从源代码开始。实现代码,保存并运行。运行的进程依次从编译器开始。编译器是一个程序,像任何其余程序同样,运行在您的机器上。而后遍历全部代码并生成目标文件。那些文件就是机器代码。他们优化了运行在特定机器上的代码,这就是为何当你从一个操做系统移动到另外一个操做系统时,你必须使用特定的编译器。

可是您不能执行单独的目标文件,您须要将它们合并成一个单独的文件,即众所周知的.exe文件(可执行文件)。那是连接器(linker)的工做。

最后,加载器是负责将exe文件中的代码传输到操做系统虚拟内存的代理。它基本上是一个转运体。在这里,您的程序终于启动并运行了。

听起来像是一个漫长的过程,不是吗?

在大多数状况下(除非您是在银行大型机中使用Assembly的开发人员),您都将花费时间用高级语言进行编程:Java,C#,Ruby,JavaScript等。

语言越高,速度越慢。这就是为何 C 和 C++ 要快得多,它们很是接近机器代码语言:汇编语言。

除了性能以外,V8的一个主要好处是能够超越ECMAScript标准,也能够理解c++

figure02.png

JavaScript 受限于 ECMAScript。而V8为了生存,就必须兼容但不限于它。

在V8中具备很是棒的集成C++特性的能力。C++已经发展的很是好的OS操做:文件处理和内存/线程处理,在JavaScript中拥有全部这些能力是很是有用的。

若是您仔细想一想,Node.js自己就是以相似的方式诞生的。 它采用了相似的方式来升级到V8,再加上服务器和网络功能。

二、单线程

若是您是Node开发人员,那么您应该很清楚V8的单线程性质。 每一个JavaScript执行上下文都与一个线程成正比。

固然,V8在后台管理OS线程机制。 它是一个复杂的软件,而且能够同时执行许多工做,所以可使用多个线程。

咱们有一个执行代码的主线程,另外一个用于编译代码的线程(是的,每次编译新代码时咱们都没法中止执行),还有一些用于处理垃圾回收,等等。

然而,V8为每一个JavaScript执行上下文建立了一个单线程环境。其他的都在它的控制之下。

想象一下您应该执行JavaScript代码的函数调用栈。 JavaScript经过按照插入/调用每一个函数的顺序将一个函数堆叠在另外一个函数之上来工做。 在介绍每一个功能的内容以前,咱们没法知道它是否调用了其余功能。 若是发生这种状况,那么被调用的函数将被放置在堆栈中调用者以后。

例如,当涉及到回调时,它们被放置在堆栈的末尾。

V8的主要任务之一是管理该堆栈组织和该过程所需的内存。

三、Ignition 和 TurboFan

自2017年5月发布5.9版以来,V8附带了一个新的JavaScript执行管道,该管道基于V8的解释器Ignition构建。 它还包括更新更好的优化编译器⁠-TurboFan。

这些变化彻底集中在总体性能上,以及Google开发人员在调整引擎以适应JavaScript领域带来的全部快速而显著的变化时所面临的困难。

从项目一开始,V8的维护者就一直在担忧可否找到一种好方法来提升V8的性能,使其与JavaScript的发展速度保持一致。

如今咱们能够看到,在运行新引擎时,与最大的基准测试相比,有了巨大的改进

figure03.png

来源: https://v8.dev/blog/launching...*

你能够在这里和这里阅读更多关于 Ignition and TurboFan

四、隐藏类

这是V8的另外一个魔术。JavaScript是一种动态语言。这意味着能够在执行期间添加、替换和删除新属性。这在Java这样的语言中是不可能的,例如,全部的东西(类、方法、对象和变量)都必须在程序执行以前定义,而且不能在应用程序启动后动态更改。

因为其特殊的性质,JavaScript解释器一般根据散列函数执行字典查找,以确切知道该变量或对象在内存中的分配位置。

这在最终过程当中花费不少。 在其余语言中,建立对象时,它们会收到一个地址(指针)做为其隐式属性之一。 这样,咱们就确切知道它们在内存中的放置位置以及要分配的空间。

使用 JavaScript,这是不可能的,由于咱们不能映射不存在的东西。这就是隐藏类统治的地方。

隐藏类几乎和 Java 中的同样:静态类和固定类都有一个惟一的地址来定位它们。然而,V8并非在程序执行以前执行,而是在运行时执行,每当对象结构发生动态变化时。

让咱们看一个例子来明白问题。 考虑如下代码片断:

function User(name, fone, address) {
   this.name = name
   this.phone = phone
   this.address = address
}

在 JavaScript 基于原型的性质下,每次咱们实例化一个新的 User 对象时,能够说:

var user = new User("John May", "+1 (555) 555-1234", "123 3rd Ave")

而后,V8建立一个新的隐藏类。 咱们称它为_User0。

figure04.png

每一个对象在内存中都有一个对其类表示的引用。它是类指针。此时,由于咱们刚刚实例化了一个新对象,因此在内存中只建立了一个隐藏类。它如今是空的。

当您执行此函数中的第一行代码时,将基于上一个隐藏类(此次是 _User1)建立一个新的隐藏类。

figure05.png

基本上,它是具备name 属性的 User 的内存地址。 在咱们的示例中,咱们并无使用仅具备名称的用户做为属性,可是每次您使用它时,都会加载隐藏的类V8做为参考。

name 属性被添加到内存缓冲区的偏移量0中,这意味着它将被视为最终顺序中的第一个属性。

V8 will also add a transition value to the _User0 hidden class. This helps the interpreter to understand that every time a name property is added to a User object, the transition from _User0 to _User1 must be addressed.

V8还将向_User0隐藏类添加一个过渡值。 这有助于解释器理解,每次将name属性添加到User对象时,都必须吹里从_User0_User1的转换。

当调用函数的第二行时,相同的过程再次发生,并建立一个新的隐藏类:

figure06.png

您能够看到,隐藏类跟踪堆栈。在由转换值维护的链中,一个隐藏类致使另外一个隐藏类。

属性添加的顺序决定了V8将要建立多少隐藏类。若是您更改咱们建立的代码片断中的行顺序,还将建立不一样的隐藏类。这就是为何一些开发人员试图维护重用隐藏类的顺序,从而减小开销。

五、高速缓存

这是 JIT(Just In Time)编译器世界中很是广泛的术语。 它与隐藏类的概念直接相关。

例如,每次调用将对象做为参数传递的函数时,V8都会查看此操做并思考:“嗯,该对象已成功两次或屡次做为参数传递给该函数……为何不将其存储在 个人缓存用于未来的调用,而不是再次执行整个耗时的隐藏类验证过程?”

让咱们回顾一下最后一个例子:

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
}

在将实例化的任意值做为参数的用户对象两次发送给一个函数后,V8将跳过隐藏类查找并直接转到偏移量的属性。这要快得多。

可是,请记住,若是在函数中更改任何属性分配的顺序,则会产生不一样的隐藏类,所以V8将没法使用高速缓存功能。

这是一个很好的例子,说明开发人员不该该不更深刻地了解引擎。相反,拥有这些知识将帮助您的代码执行得更好。

六、垃圾回收

你还记得咱们提到的V8在不一样的线程中收集内存垃圾吗?因此,这颇有帮助,由于咱们的程序执行不会受到影响。

V8使用众所周知的“标记-清除策略”来收集内存中的死对象和旧对象。在这种策略中,GC扫描内存对象并将其标记为收集的阶段有点慢,由于它暂停执行以实现收集。

然而,V8是增量执行的。对于每一个GC中止,V8尝试标记尽量多的对象。它使一切都更快,由于在收集完成以前不须要中止整个执行。在大型应用程序中,性能改进会带来很大的不一样。

相关文章
相关标签/搜索