We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
两周前我们开始了理解Javascript及其如何工作的研究:我们认为如果了解了JavaScript的构造模块和这些模块是如何一起工作的,这将会帮助我们写出更好的代码和应用。
此系列的第一篇文章意在提供一个JavaScript引擎,运行时间和调用栈的预览。这篇文章将会深入Google v8引擎。我们也会提供一些关于写出更好JavaScript代码的建议 —— 来自我们开发团构建SessionStack的最佳实践。
一个JavaScript引擎是执行JavaScript代码的程序或者解释器。关于JavaScript引擎的实现,你可以用一个标准的解释器实现,也可以实时编译 —— 将JavaScript代码编译成某种字节码。
这是目前热门的JavaScript引擎列表:
V8引擎是开源的,由Google公司通过C++实现。V8引擎不但被使用在Chrome内,而且在node中也同样被使用。 V8在初次设计时是为了提高JavaScript在浏览器的执行性能。为了提高执行速度,V8将JavaScript代码翻译成了更高效的机器码而不是交给解释器。它通过实现一个即时编译器来把JavaScript代码编译成机器码,就像当前很多JavaScript引擎一样,例如:SpiderMonkey或者Rhino(Mozilla)。和其他引擎不同的是V8不产生其他的字节码或者任何中间代码。
在V8的5.9版本之前,V8使用过两个编译器:
V8引擎还在内部使用了几类线程
当第一次执行JavaScript代码,V8引擎利用full-codegen直接把解析过的JavaScript代码编译成机器代码,并且没有任何中间转换。这使得可以很快速的执行机器代码。注意V8引擎并没有使用任何中间字节码,这代表这种方式的执行并不依赖任何解释器。
当代码运行一段时间后,解析器已经收集了足够多的数据,并且找出哪个方法需要被优化。
接下来,Crankshaft优化器在另一个线程开始了。它首先把AST(抽象语法树)转换成高级的中间表示Hydrogen(SSA-静态单赋值),然后试图优化Hydrogen图。大多数的优化发生在此阶段。(译者注:SSA概念)
第一个优化就是预先内联尽可能多的代码。内联就是用函数主体替换调用此函数的调用点的过程。这简单的一步使接下来的优化变的更有意义。
JavaScript是一种基于原型的语言:没有类和对象是使用克隆过程创建的。JavaScript也是一种动态编程语言,这意味着在实例化之后,可以很容易地从对象中添加或删除属性。
大多数JavaScript解释器使用类似字典的结构(基于哈希函数)来存储对象属性值在内存中的位置。这种结构使得在JavaScript中检索一个属性的值比Java或c#之类的非动态编程语言要昂贵得多。在Java中,所有对象属性都是在编译之前由固定的对象内容决定的,在运行时不能动态添加或删除(好,c#拥有动态类型,这是另一个主题)。因此,属性的值(或指向这些属性的指针)可以作为内存中的一个连续缓冲区存储,并在每个内存中固定偏移量。可以很容易地根据属性类型确定偏移量的长度,而在JavaScript中,这是不可能的,因为在运行时属性类型可以更改。
由于使用字典查找内存中对象属性的位置非常低效,V8使用了一种不同的方法:隐藏类。隐藏类与Java等语言中使用的固定对象内容(类)类似,只不过它们是在运行时创建的。现在,让我们看看它们到底是什么样子的:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2);
一旦 "new Point(1, 2)" 被调用,V8将会创建一个叫做"C0"的隐藏类。
还没有给Point定义属性,所以"C0"是空的。
当第一条语句"this.x = x"执行,V8将会创建第二个以"C0"为基础的,叫做"C1"的隐藏类。"C1"描述了属性X在内存中(相对于对象指针)的位置。在这种情况下,"x"被存储在偏移量为0的位置上,这意味着当将内存中的一个Ponit对象视为连续缓冲区时,第一个偏移量将对应于属性"x"。V8还将使用一个“类转换”更新“C0”,它声明如果将一个属性“x”添加到一个点对象中,隐藏的类应该从“C0”切换到“C1”。下面的point对象的隐藏类现在是“C1”。
每当一个新属性被添加到一个对象时,旧的隐藏类就会被更新到新的隐藏类的转换路径中。隐藏类转换非常重要,因为它们允许在创建相同方法的对象之间共享隐藏的类。如果两个对象共享了一个隐藏的类,并且将相同的属性添加到这两个对象中,那么这种类转换将确保两个对象都收到相同的新隐藏类和随之而来的所有优化的代码.
当“this.y = y”执行时,上面的这个过程会重复。
一个新的隐藏类"C2"就创建了,一个类转换被添加到"C1"中,声明如果将一个属性"y"添加到Point对象(已经包含属性"x"),那么隐藏的类应该改为"C2",Point对象的隐藏类被更新为"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;
现在,您可以假设对于p1和p2都将使用相同的隐藏类和转换。好吧,其实不是。对于“p1”来说,首先要添加属性“a”,然后属性“b”。然而,对于“p2”,第一个“b”被赋值,然后是“a”。因此,“p1”和“p2”由于不同的转换路径而最终得到不同的隐藏类。在这种情况下,最好以相同的顺序初始化动态属性,这样就可以重用隐藏的类。
V8利用了另一种优化动态类型语言的技术,称为内联缓存。内联缓存来自于这样的观察,即对同一种方法的重复调用往往发生在同一类型的对象上。可以在这里找到对内联缓存的深入解释。
我们将讨论内联缓存的一般概念(如果您没有足够的时间来完成上面的深入解释)。
那么它是如何工作的呢?V8把最近方法调用中,作为参数传递的对象类型进行了缓存,并且根据这些信息对之后参数传递中可能出现的对象的类型进行假设。如果V8能够对一个作为参数的对象的类型进行很好的假设,它就可以绕过如何访问对象属性的过程,直接使用之前存储的查找对象的隐藏类时的信息。
那么隐藏类和内联缓存的概念是如何相关的呢?每当在一个特定的对象上调用一个方法时,V8引擎就必须对该对象的隐藏类执行一次查找,以确定访问特定属性的偏移量。在对同一个隐藏类的同一个方法进行了两次成功调用之后,V8省略了隐藏类查找,并简单地将该属性的偏移量添加到对象指针本身。对于该方法的之后的调用,V8引擎假设隐藏的类没有发生变化,并且使用从先前的查找中存储的偏移量直接跳转到特定属性的内存地址。这大大提高了执行速度。
内联缓存也是为什么相同类型的对象共享隐藏类非常重要的原因。如果您创建相同类型的两个对象和不同隐藏类(正如我们前面的例子,由于属性声明的顺序不一致),V8不会使用内联缓存,因为即使两个相同类型的对象,相应的隐藏类也会为属性分配不同的偏移量。
一旦Hydrogen图被优化,Crankshaft将其降低到一个称为Lithium的较低级别表示。大多数Lithium实现都是针对架构的。寄存器分配发生在这一级。
在最后,Lithium被编译成了机器码。然后OSR(on-stack replacement,当前栈替换)发生了。当我们要开始编译和优化一个明显要长时间运行的方法之前,我们很可能运行过它。V8不会忘记刚刚运行过的代码,也不会重新使用优化后的版本再次运行。相反,它将转换我们拥有的所有上下文(堆栈、寄存器),这样我们就可以在执行的过程中就切换到优化的版本。这是一个非常复杂的任务,不像其他的优化,V8在一开始就内联了代码。V8并不是唯一能够做到这一点的引擎。
有一种叫做“去优化”的保护措施,可以进行相反的转换,并回归到非优化的代码中,以防引擎做出的假设不再成立。
对于垃圾收集来说,V8使用了传统的标记-清理的方法来清理旧的一代。标记阶段应该停止JavaScript的执行。为了控制GC成本并使执行更加稳定,V8使用增量标记:不遍历整个堆,尝试标记每一个可能的对象,它只会遍历一部分堆,然后恢复正常执行。下一个GC停止将继续从先前的堆遍历停止的地方继续。这允许在正常执行期间出现非常短的暂停。如前所述,清扫阶段是由单独的线程处理的。
在2017年早些时候发布的V8 5.9版本中,引入了一条新的执行管道。这个新管道在实际的JavaScript应用程序中实现了更大的性能改进和显著的内存节省。
新的执行管道是在V8的解释器Ignition,和V8最新的优化编译器TurboFan的基础上建立的。
点击这里可以查看更多V8团队中关于这个主题的博客文章。
自从5.9版本的V8推出以来,V8引擎已经不再使用full-codeget 和 Crankshaft(自2010年以来一直为V8提供服务的技术),因为V8团队一直在努力跟上新的JavaScript语言特性和这些特性所需要的优化。
这意味着整体V8将会有更简单、更易于维护的架构。
这些改进仅仅是个开始。新的Ignition和TurboFan管道为进一步的优化铺平了道路,这将在未来几年内提高JavaScript的性能,并缩小在Chrome和Node上的V8的内存占用。
最后,这里有一些关于如何编写优化的、更好的JavaScript的技巧和技巧。你可以很容易地从上面的内容中推导出这些内容,但是为了方便,这里还是有一个便于你使用的总结:
作者:Alexander Zlatkov
编译:mike
英文原文: How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code
The text was updated successfully, but these errors were encountered:
auto-archiving for issue #240
d93856f
No branches or pull requests
JavaScript是如何工作的:深入V8引擎 + 5个优化代码的建议
两周前我们开始了理解Javascript及其如何工作的研究:我们认为如果了解了JavaScript的构造模块和这些模块是如何一起工作的,这将会帮助我们写出更好的代码和应用。
此系列的第一篇文章意在提供一个JavaScript引擎,运行时间和调用栈的预览。这篇文章将会深入Google v8引擎。我们也会提供一些关于写出更好JavaScript代码的建议 —— 来自我们开发团构建SessionStack的最佳实践。
预览
一个JavaScript引擎是执行JavaScript代码的程序或者解释器。关于JavaScript引擎的实现,你可以用一个标准的解释器实现,也可以实时编译 —— 将JavaScript代码编译成某种字节码。
这是目前热门的JavaScript引擎列表:
为什么要创建V8引擎?
V8引擎是开源的,由Google公司通过C++实现。V8引擎不但被使用在Chrome内,而且在node中也同样被使用。
V8在初次设计时是为了提高JavaScript在浏览器的执行性能。为了提高执行速度,V8将JavaScript代码翻译成了更高效的机器码而不是交给解释器。它通过实现一个即时编译器来把JavaScript代码编译成机器码,就像当前很多JavaScript引擎一样,例如:SpiderMonkey或者Rhino(Mozilla)。和其他引擎不同的是V8不产生其他的字节码或者任何中间代码。
V8曾经有两种编译器
在V8的5.9版本之前,V8使用过两个编译器:
V8引擎还在内部使用了几类线程
当第一次执行JavaScript代码,V8引擎利用full-codegen直接把解析过的JavaScript代码编译成机器代码,并且没有任何中间转换。这使得可以很快速的执行机器代码。注意V8引擎并没有使用任何中间字节码,这代表这种方式的执行并不依赖任何解释器。
当代码运行一段时间后,解析器已经收集了足够多的数据,并且找出哪个方法需要被优化。
接下来,Crankshaft优化器在另一个线程开始了。它首先把AST(抽象语法树)转换成高级的中间表示Hydrogen(SSA-静态单赋值),然后试图优化Hydrogen图。大多数的优化发生在此阶段。(译者注:SSA概念)
内联
第一个优化就是预先内联尽可能多的代码。内联就是用函数主体替换调用此函数的调用点的过程。这简单的一步使接下来的优化变的更有意义。
隐藏类
JavaScript是一种基于原型的语言:没有类和对象是使用克隆过程创建的。JavaScript也是一种动态编程语言,这意味着在实例化之后,可以很容易地从对象中添加或删除属性。
大多数JavaScript解释器使用类似字典的结构(基于哈希函数)来存储对象属性值在内存中的位置。这种结构使得在JavaScript中检索一个属性的值比Java或c#之类的非动态编程语言要昂贵得多。在Java中,所有对象属性都是在编译之前由固定的对象内容决定的,在运行时不能动态添加或删除(好,c#拥有动态类型,这是另一个主题)。因此,属性的值(或指向这些属性的指针)可以作为内存中的一个连续缓冲区存储,并在每个内存中固定偏移量。可以很容易地根据属性类型确定偏移量的长度,而在JavaScript中,这是不可能的,因为在运行时属性类型可以更改。
由于使用字典查找内存中对象属性的位置非常低效,V8使用了一种不同的方法:隐藏类。隐藏类与Java等语言中使用的固定对象内容(类)类似,只不过它们是在运行时创建的。现在,让我们看看它们到底是什么样子的:
一旦 "new Point(1, 2)" 被调用,V8将会创建一个叫做"C0"的隐藏类。
还没有给Point定义属性,所以"C0"是空的。
当第一条语句"this.x = x"执行,V8将会创建第二个以"C0"为基础的,叫做"C1"的隐藏类。"C1"描述了属性X在内存中(相对于对象指针)的位置。在这种情况下,"x"被存储在偏移量为0的位置上,这意味着当将内存中的一个Ponit对象视为连续缓冲区时,第一个偏移量将对应于属性"x"。V8还将使用一个“类转换”更新“C0”,它声明如果将一个属性“x”添加到一个点对象中,隐藏的类应该从“C0”切换到“C1”。下面的point对象的隐藏类现在是“C1”。
每当一个新属性被添加到一个对象时,旧的隐藏类就会被更新到新的隐藏类的转换路径中。隐藏类转换非常重要,因为它们允许在创建相同方法的对象之间共享隐藏的类。如果两个对象共享了一个隐藏的类,并且将相同的属性添加到这两个对象中,那么这种类转换将确保两个对象都收到相同的新隐藏类和随之而来的所有优化的代码.
当“this.y = y”执行时,上面的这个过程会重复。
一个新的隐藏类"C2"就创建了,一个类转换被添加到"C1"中,声明如果将一个属性"y"添加到Point对象(已经包含属性"x"),那么隐藏的类应该改为"C2",Point对象的隐藏类被更新为"C2"。
隐藏类转换是由对象内属性添加的顺序决定的。看一下下面的代码:
现在,您可以假设对于p1和p2都将使用相同的隐藏类和转换。好吧,其实不是。对于“p1”来说,首先要添加属性“a”,然后属性“b”。然而,对于“p2”,第一个“b”被赋值,然后是“a”。因此,“p1”和“p2”由于不同的转换路径而最终得到不同的隐藏类。在这种情况下,最好以相同的顺序初始化动态属性,这样就可以重用隐藏的类。
内联缓存
V8利用了另一种优化动态类型语言的技术,称为内联缓存。内联缓存来自于这样的观察,即对同一种方法的重复调用往往发生在同一类型的对象上。可以在这里找到对内联缓存的深入解释。
我们将讨论内联缓存的一般概念(如果您没有足够的时间来完成上面的深入解释)。
那么它是如何工作的呢?V8把最近方法调用中,作为参数传递的对象类型进行了缓存,并且根据这些信息对之后参数传递中可能出现的对象的类型进行假设。如果V8能够对一个作为参数的对象的类型进行很好的假设,它就可以绕过如何访问对象属性的过程,直接使用之前存储的查找对象的隐藏类时的信息。
那么隐藏类和内联缓存的概念是如何相关的呢?每当在一个特定的对象上调用一个方法时,V8引擎就必须对该对象的隐藏类执行一次查找,以确定访问特定属性的偏移量。在对同一个隐藏类的同一个方法进行了两次成功调用之后,V8省略了隐藏类查找,并简单地将该属性的偏移量添加到对象指针本身。对于该方法的之后的调用,V8引擎假设隐藏的类没有发生变化,并且使用从先前的查找中存储的偏移量直接跳转到特定属性的内存地址。这大大提高了执行速度。
内联缓存也是为什么相同类型的对象共享隐藏类非常重要的原因。如果您创建相同类型的两个对象和不同隐藏类(正如我们前面的例子,由于属性声明的顺序不一致),V8不会使用内联缓存,因为即使两个相同类型的对象,相应的隐藏类也会为属性分配不同的偏移量。
编译到机器码
一旦Hydrogen图被优化,Crankshaft将其降低到一个称为Lithium的较低级别表示。大多数Lithium实现都是针对架构的。寄存器分配发生在这一级。
在最后,Lithium被编译成了机器码。然后OSR(on-stack replacement,当前栈替换)发生了。当我们要开始编译和优化一个明显要长时间运行的方法之前,我们很可能运行过它。V8不会忘记刚刚运行过的代码,也不会重新使用优化后的版本再次运行。相反,它将转换我们拥有的所有上下文(堆栈、寄存器),这样我们就可以在执行的过程中就切换到优化的版本。这是一个非常复杂的任务,不像其他的优化,V8在一开始就内联了代码。V8并不是唯一能够做到这一点的引擎。
有一种叫做“去优化”的保护措施,可以进行相反的转换,并回归到非优化的代码中,以防引擎做出的假设不再成立。
垃圾回收
对于垃圾收集来说,V8使用了传统的标记-清理的方法来清理旧的一代。标记阶段应该停止JavaScript的执行。为了控制GC成本并使执行更加稳定,V8使用增量标记:不遍历整个堆,尝试标记每一个可能的对象,它只会遍历一部分堆,然后恢复正常执行。下一个GC停止将继续从先前的堆遍历停止的地方继续。这允许在正常执行期间出现非常短的暂停。如前所述,清扫阶段是由单独的线程处理的。
Ignition and TurboFan
在2017年早些时候发布的V8 5.9版本中,引入了一条新的执行管道。这个新管道在实际的JavaScript应用程序中实现了更大的性能改进和显著的内存节省。
新的执行管道是在V8的解释器Ignition,和V8最新的优化编译器TurboFan的基础上建立的。
点击这里可以查看更多V8团队中关于这个主题的博客文章。
自从5.9版本的V8推出以来,V8引擎已经不再使用full-codeget 和 Crankshaft(自2010年以来一直为V8提供服务的技术),因为V8团队一直在努力跟上新的JavaScript语言特性和这些特性所需要的优化。
这意味着整体V8将会有更简单、更易于维护的架构。
这些改进仅仅是个开始。新的Ignition和TurboFan管道为进一步的优化铺平了道路,这将在未来几年内提高JavaScript的性能,并缩小在Chrome和Node上的V8的内存占用。
最后,这里有一些关于如何编写优化的、更好的JavaScript的技巧和技巧。你可以很容易地从上面的内容中推导出这些内容,但是为了方便,这里还是有一个便于你使用的总结:
如何编写优化的JavaScript
作者:Alexander Zlatkov
编译:mike
英文原文: How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code
The text was updated successfully, but these errors were encountered: