做者:Mythri Alle, Dan Elphick, and Ross McIlroy翻译:疯狂的技术宅前端
原文:https://v8.dev/blog/v8-lite程序员
未经容许严禁转载面试
在 2018 年底,为了大幅减小 V8 的内存使用量,咱们启动了一个名为 V8 Lite 的项目。该项目最初被设想为 V8 的一个独立的 精简模式(Lite mode),专门针对低内存移动设备或嵌入式用例,这些用例更关心的是减小内存的使用而不是吞吐量的执行速度。可是在进行这项工做的过程当中,咱们意识到为Lite 模式所作的许多内存优化均可以转移到常规 V8 中,从而使 V8 的全部用户受益。segmentfault
本文重点介绍了咱们开发的一些关键优化以及它们在实际工做负载中对内存所作的优化。数组
注意:若是您不喜欢阅读文章,请欣赏下面的视频!浏览器
Ross McIlroy在BlinkOn 10上发表的 “V8 Lite – 减小 JavaScript 内存”。缓存
https://www.youtube.com/embed...服务器
为了优化 V8 的内存使用,咱们首先须要了解 V8 如何使用内存以及哪些对象类型在 V8 堆中占了很大的比例。咱们用了 V8 的内存可视化工具来跟踪许多典型网页的堆内容的构成。微信
加载印度时报时,不一样对象类型所使用的 V8 堆的百分比多线程
为此,咱们肯定了对 JavaScript 执行并非必不可少的对象在 V8 堆中占了很大一部分 ,可是这些对象被用于优化 JavaScript 执行,并处理特殊状况。例如:优化的代码;类型反馈,用于肯定如何优化代码;用于在 C++ 和 JavaScript 对象之间进行绑定的冗余元数据;仅在特殊状况下才须要元数据,如堆栈跟踪符号;还有在页面加载期间仅执行几回的函数的字节码。
结果,咱们开始在 V8 的 精简模式 上进行工做,该模式经过大幅减小这些可选对象的分配来权衡 JavaScript 执行的速度与节省的内存。
经过配置现有的 V8 设置,能够对精简模式进行许多更改,例如禁用 V8 的 TurboFan 优化编译器。可是其余的优化还须要对 V8 进行更多的修改。
特别是,因为咱们决定在精简模式下没法优化代码,所以能够避免收集优化编译器所需的类型反馈。在 Ignition 解释器中执行代码时,V8 会收集有关传递给各类操做的操做数类型(例如,+
或 o.foo
)的反馈,以便针对这些类型调整之后的优化。这些信息存储在反馈向量中,这些向量在 V8 堆内存中使用了很大的一部分。 精简模式能够避免分配这些反馈向量,可是 V8 的解释器和部份内联缓存基础结构却但愿反馈向量可用,所以还须要进行大量重构才能支持这种无反馈执行。
在 V8 的 v7.3 版本中启动的精简模式与 v7.1 相比,经过禁用代码优化,不分配反馈矢量以及执行不多执行的字节码老化(以下所述),使典型的网页堆大小减小了 22%。对于那些明显想要权衡性能以提升内存使用率的程序而言,这是一个很是不错的结果。可是在执行此项工做的过程当中,咱们意识到经过使 V8 变得更懒惰,能够实现节省精简模式的大部份内存,而不会影响性能。
彻底禁用反馈向量分配,不只会阻止 V8 的 TurboFan 编译器对代码进行优化,并且还会阻止 V8 执行常见操做(例如对象)的 inline caching 属性在 Ignition 解释器中的加载。因此这样作会大大下降 V8 的执行时间,在典型的交互式网页方案中,页面加载时间减小了 12%,而 V8 使用的 CPU 时间增长了120%。
为了在不进行这些回归的状况下将节省的大部份内存用于常规 V8,咱们转而采用了另外一种方法,在该函数执行了必定数量的字节码(当前为1KB)以后,开始惰性分配反馈向量。因为大多数函数并非要常常执行,所以在大多数状况下,咱们避免分配反馈矢量,而是在须要的地方快速分配它们,以免性能降低,而且仍然能够对代码进行优化。
这种方法的另外一个复杂性与如下事实有关:反馈向量造成一棵树,内部函数的反馈向量被保留为外部函数的反馈向量中的条目。这是很是必要的,这样可使新建立的函数闭包与为同一函数建立的全部闭包同样,接收相同的反馈矢量数组。在惰性分配反馈向量的状况下,咱们没法用反馈向量来造成这棵树,由于没法保证外部函数会在内部函数分配其反馈向量以前就对其进行分配。为了解决这个问题,咱们建立了一个新的 ClosureFeedbackCellArray
来维护这棵树,而后在函数变热时用一个完整的 FeedbackVector
换出一个函数的 ClosureFeedbackCellArray
。
惰性反馈分配先后的反馈矢量树
咱们实验和现场测试结果代表,在台式机上的惰性反馈没有出现性能降低的趋势,而在移动平台上,因为减小了垃圾收集,实际上在低端设备上性能有所提升。所以咱们在全部 V8 版本中都启用了惰性反馈分配,其中包括精简模式,与咱们原始的无反馈分配方法相比,内存模式略有退步,可是实际性能却获得了很大的提升。
从 JavaScript 编译字节码时,会生成把字节码序列与 JavaScript 源码中的字符位置相关联的源位置表。可是仅在符号化异常或执行开发人员任务(例如调试)时才须要此信息,所以不多使用。
为了不这种浪费,如今编译字节码时不收集源位置(假设未链接调试器或分析器),仅在实际生成堆栈跟踪时(例如,在调用 Error.stack
或将异常的栈跟踪打印到控制台时)才收集源。这确实须要付出一些代价,由于生成源位置须要从新解析和编译函数,可是大多数网站并未在生产中使用栈跟踪符号,因此看不到什么可以观察到的性能影响。
咱们必须解决的一个问题是须要可重复的字节码生成,而这是之前没法保证的。若是 V8 在收集源位置时与原始代码生成不一样的字节码,则源位置不对齐,而且堆栈跟踪可能指向源代码中的错误位置。
在某些状况下,因为在函数在先急速解析再延迟编译时丢失了一些解析信息,V8 可能会根据某个函数是急速仍是延迟编译来生成不一样的字节码。这些不匹配大可能是良性的,例如,忘记了变量是不可变的事实,所以没法对其进行优化。可是,这项工做发现的某些不匹配在某些状况下确实有可能致使代码错误的执行。所以,咱们修复了这些不匹配问题,并添加了检查和压力模式,以确保函数的急速和惰性编译始终可以产生一致的输出,从而使咱们对 V8 解析器和预解析器的正确性和一致性更具信心。
从 JavaScript 源码编译的字节码占据了 V8 堆空间的很大一部分,一般大约为 15%,其中包括相关的元数据。有许多函数仅在初始化的时候执行,或者在编译后不多被使用。
因此咱们添加了对垃圾回收期间从函数中清除编译后的字节码的支持,若是它们最近没有执行过的话。为此咱们要跟踪函数字节码的 age,增长每一个 major(mark-compact)垃圾回收的 age,并在执行该函数时将其重置为零。任何超过老化阈值的字节码均可以在下一次垃圾回收中被收集。若是已收集了,可是稍后须要再次执行,那么将会从新编译它。
要确保只在再也不须要字节码时才刷新它存在着技术难题。若是函数 A
调用另外一个长期运行的函数 B
,则函数 A
可能会在其仍在堆栈中时老化。即便函数 A
达到了老化阈值咱们也不但愿刷新它的字节码,由于咱们须要在长时间运行的函数 B
返回到 A
。所以当字节码达到函数的老化阈值时,咱们会将其视为函数的弱保留,而堆栈或其余位置对它的任何引用都做为强保留。咱们仅在没有强连接剩余时才刷新代码。
除了刷新字节码,咱们还刷新与这些刷新函数关联的反馈向量,可是咱们没法在与字节码相同的 GC 周期内刷新它们,由于它们没有被同一对象保留。字节码由与本机上下文无关的 SharedFunctionInfo
保留,而反馈向量则由依赖于本机上下文的 JSFunction
保留。最后咱们在随后的 GC 周期中刷新反馈向量。
通过两个GC循环后,老化的函数的对象布局
除了这些较大的项目,咱们还发现并解决了一些致使效率低下的问题。
第一个是减少 FunctionTemplateInfo
对象的大小。这些对象存储与 FunctionTemplate
有关的内部元数据,这些元数据用于使嵌入程序(例如 Chrome)提供可被调用的函数的 C++ 回调实现。经过 JavaScript 代码。 Chrome 浏览器引入了许多 FunctionTemplates
以实现 DOM Web API,所以,FunctionTemplateInfo
对象对 V8 的堆大小有所贡献。在分析 FunctionTemplates
的典型用法以后,咱们发如今 FunctionTemplateInfo
对象上的11个字段中,一般只有 3 个被设置为非默认值。所以咱们拆分了 FunctionTemplateInfo
对象,以便将稀有字段存储在边表中,该边表仅在须要时才按需分配。
第二个优化与如何取消 TurboFan 的代码优化有关。因为 TurboFan 执行推测性优化,因此若是某些条件再也不成立,则可能须要回退到解释器(取消优化)。每一个取消点都有一个 ID,该 ID 可使运行时可以肯定字节码应该把执行返回到解释器中的哪一个位置上。之前经过优化代码跳转到大型跳转表中的特定偏移量来计算这个 ID,而后再将正确的 ID 加载到寄存器中,最后跳转到运行时以执行反优化。这样作的好处是,对于每一个取消点,在优化代码中只须要一条跳转指令。可是,取消优化跳转表已经预先分配,而且它必须足够大,这样才能支持整个取消优化 id 的范围。因此咱们修改了 TurboFan,使优化代码中的 deopt 点在调用运行时以前能够直接加载 deopt id。这样咱们就可以彻底删除这个大型跳转表,可是代价是须要略微增长优化代码的大小。
咱们已经在 V8 最后七个版本中发布了上述优化。一般,它们首先以精简模式开始,而后又被带到 V8 的默认配置。
AndroidGo设备上一组典型网页的 V8 堆的平均大小
与v7.1(Chrome 71)相比,V8 的 v7.8(Chrome 78)版本每种页面的内存节省状况详情
在这段时间里,咱们在一系列典型网站上将 V8 堆大小平均减小了 18%,这对应于低端 AndroidGo 移动设备,平均减小了 1.5 MB。在基准测试或实际的网页交互中,这对 JavaScript 性能可能并无什么重大影响。
精简模式能够经过禁用函数优化来进一步节省内存,但会以必定的成本提升 JavaScript 执行吞吐量。平均而言,精简模式可节省 22% 的内存,而某些页面最多可节省 32%。这对应于 AndroidGo 设备上的 V8 堆大小减小了 1.8 MB。
与 v7.1(Chrome 71)相比,V8 v7.8(Chrome 78)的内存用量减小了
当把每一个优化的影响分开来看时,很明显,不一样的页面会从每个优化中得到不一样比例的收益。展望将来,咱们将继续寻找潜在的优化方案,这些优化方案能够进一步减小 V8 对内存的使用量,同时仍然保持 JavaScript 惊人的执行速度。