【译】深入了解V8

简介: 【译】深入了解V8

本文是一篇译文。无关内容已删减。

原文标题:A Deep Dive Into V8

原文链接:https://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


正文开始


大部分前端开发人员都会遇到一个流行词:V8。它的流行程度很大一部分是因为它将JavaScript的性能提升到了一个新的水平。

是的,V8很快。但它是如何发挥它的魔力?为什么它反应如此迅速呢?

官方文档指出:V8是谷歌开源高性能JavaScript和WebAssembly引擎,用C++编写。它主要用在Chrome和Node.js中,等等。

换句话说,V8是一种C++开发的软件,它将JavaScript编译成可执行代码,即机器码。

现在,我们开始看得更清楚,Chrome和Node.js只是一个桥梁,负责把JS代码运送到最终的目的地:在特定机器上运行的机器码。

V8性能的另一个重要角色是它的分代和超精确的垃圾收集器。它被优化为使用低内存收集JavaScript不再需要的对象。

除此之外,V8还依靠一组其他的工具和特性来改进JS的一些固有功能。这些功能往往会使JS变慢(例如JS的动态特性)。

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


从基础的开始

640.png

机器码是如何工作的呢?简单地说,机器代码是在机器内存的特定部分执行的一组非常低级的指令。

生成机器码的过程,用C++举例,大概像下面这样:


在进一步讨论之前,必须指出这是一个编译过程,它不同于JavaScript解释过程。实际上,编译器在进程结束时生成一个完整的程序,而解释器作为一个程序本身工作,它通过读取指令(通常是脚本,如JavaScript脚本)并将其转换为可执行命令来完成任务。

解释过程可以是动态的(解释器解析并只运行当前命令)或完全解析(即解释器在继续执行相应的机器指令之前首先完全翻译脚本)。

回到图中,编译过程通常从源代码开始。你实现代码,保存并运行。运行的进程依次从编译器开始。编译器是一个程序,和其他程序一样,运行在你的机器上。然后它遍历所有代码并生成对象文件。那些文件是机器代码。它们是在特定机器上运行的优化代码,这就是为什么当你从一个操作系统转移到另一个操作系统时必须使用特定的编译器。

但是你不能执行单独的对象文件,你需要把它们组合成一个文件,即众所周知的.exe文件(可执行文件)。这是Linker的工作。

最后,Loader是代理,负责将exe文件中的代码传输到操作系统的虚拟内存中。它基本上是一个运输工具。在这里,你的程序终于开始运行了。

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

大多数时候(除非你是在银行大型机上使用汇编的开发人员),你会花时间用高级语言编程:Java、C#、Ruby、JavaScript等。

语言越高级,速度越慢。这就是为什么C和C++速度更快,因为它们非常接近机器代码语言:汇编语言。

除了性能之外,V8的主要优点之一是超越ECMAScript标准的可能性,并且理解C++。


640.png

JavaScript仅限于ECMAScript。而V8引擎,为了存在,必须是兼容的,但不限于JavaScript。

具有将C++特性集成到V8中的能力是非常棒的。由于C++已经发展到非常好的OS操作的文件处理和内存/线程处理的特殊性——在JavaScript中拥有所有这些能力是非常有用的。

如果你仔细想想,Node.js它本身也是以类似的方式诞生的。它遵循与V8相似的路径,外加服务器和网络功能。


单线程


如果你是一个Node开发者,你应该很熟悉V8的单线程特性。一个JS执行上下文与线程数量成正比。

当然,V8在后台管理操作系统线程机制。它可以与多个线程一起工作,因为它是一个复杂的软件,可以同时执行许多任务。

但是,V8为每个JavaScript的执行上下文只创建一个单线程的环境。其余的都在V8的控制之下。

想象一下JavaScript代码应该进行的函数调用堆栈。JavaScript的工作原理是将一个函数堆叠在另一个函数之上,遵循每个函数的插入/调用顺序。在到达每个函数的内容之前,我们无法知道它是否调用其他函数。如果发生这种情况,那么被调用的函数将被放在堆栈中调用者的后面。

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

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


Ignition and TurboFan


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

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

从项目一开始,V8的维护人员就一直在担心如何在JavaScript不断发展的同时,找到一种提高V8性能的好方法。

现在,我们可以看到新引擎的Benchmarks测试结果,已经有了巨大提升:

640.png


Hidden Classes(隐藏类)


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

由于它的特殊性质,JavaScript解释器通常基于散列函数(hash算法)执行字典查找,以准确地知道这个变量或对象在内存中的分配位置。

这对最后一道工序来说代价很大。在其他语言中,当对象被创建时,它们接收一个地址(指针)作为其隐式属性之一。这样,我们就可以准确地知道它们在内存中的位置以及要分配多少空间。

对于JavaScript,这是不可能的,因为我们无法映射出不存在的内容。这就是Hidden Classes发挥作用的地方。

隐藏类与Java中的类几乎相同:静态类和固定类具有唯一的地址来定位它们。然而,V8并不是在程序执行之前执行,而是在运行过程中,每次对象结构发生“动态变化”时执行。

让我们看一个例子来说明问题。考虑以下代码片段:


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


在JavaScript基于原型的特性中,每次实例化一个新的用户对象时,假设:


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


然后V8创建一个新的隐藏类。我们称之为_User0

640.png


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

当你在这个函数中执行第一行代码时,将在上一个基础上创建一个新的隐藏类,这次是_User1


640.png


它基本上是具有name属性的User的内存地址。在我们的示例中,我们没有使用仅将name作为属性的user,但每次这样做时,这就是V8将作为引用加载的隐藏类。

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


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

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

640.png



你可以看到隐藏类跟踪堆栈。在由转换值维护的链中,一个隐藏类通向另一个。

属性添加的顺序决定了V8将要创建多少个隐藏类。如果您更改我们所创建的代码段中的行的顺序,那么也将创建不同的隐藏类。这就是为什么有些开发人员试图保持重用隐藏类的顺序,从而减少开销。


Inline Caching(内联缓存)


这是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}


当我们将User对象的实例两次作为参数传递给函数后,V8将跳转到隐藏类查找并直接转到偏移量的属性。这要快得多。

但是,请记住,如果更改函数中任何属性赋值的顺序,则会导致不同的隐藏类,因此V8将无法使用内联缓存功能。

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


Garbage Collecting(垃圾回收)


你还记得我们提到过V8在另一个线程中收集内存垃圾吗?这很有帮助,因为我们的程序执行不会受到影响。

V8使用众所周知的“标记和扫描”策略来收集内存中的旧对象。在这种策略中,GC扫描内存对象以“标记”它们以进行收集的阶段有点慢,因为这需要暂停代码执行。

但是,V8是递增的,也就是说,对于每个GC停顿,V8尝试标记尽可能多的对象。它使一切变得更快,因为在集合完成之前不需要停止整个执行。在大型应用程序中,性能的提高有很大的不同。

正文结束



关于V8更多相关内容,可以移步本公众号之前的文章:

【译】JavaScript工作原理:V8编译器的优化

【译】V8引擎的内存管理

相关文章
|
7月前
|
监控 数据挖掘 UED
1688运营实战指南:从入门到精通的学习路径全解析!
在当今电商环境下,1688作为国内领先的B2B平台,已成为众多企业不可或缺的销售渠道。无论是源头工厂、批发商,还是寻求优质货源的创业者,掌握专业的1688运营技能都显得尤为重要。本文将为大家系统梳理1688运营的学习路径和实战方法,帮助商家少走弯路,快速提升店铺运营效果。
|
8月前
|
机器学习/深度学习 人工智能 自然语言处理
88_多模态提示:图像与文本融合
在人工智能领域的快速发展中,多模态融合已成为突破单一模态限制、实现更全面智能理解的关键技术方向。人类理解世界的方式天然是多模态的——我们同时通过视觉、听觉、语言等多种感官获取信息并进行综合分析。例如,在餐厅点餐时,我们会同时处理菜单上的图片、服务员的介绍和菜品的文字描述,最终做出决策。这种自然的多模态信息整合能力,正是人工智能系统长期以来努力追求的目标。
914 0
|
编译器
区分LR(0),SLR(1),LR(1)和LALR(1)
区分LR(0),SLR(1),LR(1)和LALR(1)
2699 1
|
Oracle 架构师 分布式数据库
OceanBase数据库的发展历程是什么?
【8月更文挑战第11天】OceanBase数据库的发展历程是什么?
653 63
|
消息中间件 存储 监控
消息中间件第八讲:消息队列 RocketMQ 版实战、集群及原理
消息中间件第八讲:消息队列 RocketMQ 版实战、集群及原理
1070 0
|
缓存 Java 程序员
一个 Python 对象会在何时被销毁?
一个 Python 对象会在何时被销毁?
315 2
|
中间件 API 调度
深入探究 Python 异步编程:利用 asyncio 和 aiohttp 构建高效并发应用 精选
深入探究 Python 异步编程:利用 asyncio 和 aiohttp 构建高效并发应用 精选
488 2
|
安全 Linux 网络安全
安装和使用Paramiko
安装和使用Paramiko
1175 3
|
缓存 Java API
API接口性能优化管理
在数字化时代,API性能优化对于提升软件效率和用户体验至关重要。本文介绍了多种优化方法:配置优化包括调整JVM参数等;代码层面减少重复调用并批量操作数据库;池化技术如线程池和HTTP连接池能有效利用资源;数据库优化通过索引提高查询速度;异步处理则使主流程业务不受阻塞;缓存策略如Redis缓存减少数据库访问;可观测性工具如日志平台和APM帮助监控性能。综合运用这些方法,可根据业务需求持续调整优化,显著提升API性能及用户体验。
|
运维 监控 Docker
使用Docker进行微服务架构的部署
【5月更文挑战第18天】本文探讨了如何使用Docker进行微服务架构部署,介绍了Docker的基本概念,如容器化平台和核心组件,以及它与微服务的关系。通过Docker,每个微服务可独立运行在容器中,便于构建、测试和部署。文章详细阐述了使用Docker部署微服务的步骤,包括定义服务、编写Dockerfile、构建镜像、运行容器、配置服务通信、监控和日志管理以及扩展和更新。Docker为微服务提供了可移植、可扩展的解决方案,是现代微服务架构的理想选择。