延迟反序列化(Lazy deserialization)
延迟反序列化(Lazy deserialization)功能在 V8 6.4 中默认打开了,带来的好处就是平均每个 Chrome 的选项卡可以节约500KB左右的内存消耗。
如果你对这个话题感兴趣,可以继续往下读,否则就不用浪费时间了。
V8 快照介绍
首先,我们回顾一下 V8 是如何堆快照来加速 Isolate(基本上等同于Chrome里面的选项卡)创建的。google的郭扬有个很好的介绍文章自定义启动快照:
- JavaScript 规范包含许多内置功能,例如从数学函数到全功能的正则表达式引擎。每个新创建的 V8 上下文(V8 context)从开始就可以使用这些功能。为此,必须在创建上下文时在 V8 的堆上初始化这些功能,将全局对象(例如,浏览器中的窗口对象)和所有内置功能初始化。如果这一切都是从头开始,那么需要相当长的一段时间。
- 幸好 V8 使用了一种方法来加速这个过程:跟微波炉加一个冰冻披萨来一顿快餐差不多,V8通过反序列化一个编译好的快照来直接初始化上下文。在一台普通的桌面PC上,这个方法可以将创建上下文的时间从 40ms 缩减 2ms;在一台普通的移动设备上,这个是数字是从 270ms 到 10ms。怎么样,相当可观吧,那么你可以继续往下看了。
回顾一下:快照对于启动性能非常重要,它被用来通过反序列化为每个 Isolate 创建 V8 堆的初始状态。因此,快照的大小决定了 V8 堆的最小大小,快照越大,反序列化后每个 Isolate 就会占用更多内存。
快照包含完全初始化一个新的 Isolate 所需的一切,包括语言常量(例如未定义的值),解释器使用的内部字节码处理程序,内置对象(例如String)以及安装在内置对象上的函数(例如,String.prototype.replace)以及它们的可执行代码对象。
从2016-01到2017-09启动快照大小(以字节为单位),x轴是V8的版本。
在过去的两年中,快照的规模几乎增加了三倍,从2016年初的大约 600KB 增加到今天的超过 1500KB。绝大多数增加来自序列化的 Code 对象,这些对象的数量都有所增加(例如,随着语言规范的发展和增长,最近增加了 JavaScript 语言);和大小(由新 CodeStubAssembler 管道生成的内置插件作为本机代码与更紧凑的字节代码或最小化的 JS 格式)。
这是个坏消息,因为我们希望尽可能降低内存消耗。
延迟反序列化
一个主要痛点是将快照的全部内容复制到每个 Isolate 中。无条件地加载所有内置函数会带来资源浪费,因为某些函数可能永远都不会使用到。
延迟反序列化的概念非常简单:只在被调用之前反序列化内置函数。
对一些最受欢迎的网站的快速调查表明,这种方法非常有吸引力:平均而言,只有30%的内置功能被使用,有些网站只使用16%。这看起来非常有前景,因为这些网站大部分都是重度 JS 用户,因此这些数据可以被看作是整个网络潜在的内存节省的(模糊)下限。
当我们开始研究这个方向时,事实证明延迟的反序列化与 V8 的体系结构很好地结合在一起,并且只有少数几个非侵入式的设计更改需要启动和运行:
快照中的对象位置
。在延迟反序列化之前,序列化快照中的对象顺序是不相关的,因为我们只能一次反序列化整个堆。延迟反序列化必须能够自行反序列化任何给定的内置函数,因此必须知道它在快照中的位置。单个对象的反序列化
。 V8的快照最初是为完整的堆序列化而设计的,并且支持单个对象的反序列化需要处理一些怪癖,例如不连续的快照布局(一个对象的序列化数据可能会散布其他对象的数据)称为反向引用(可以直接引用之前在当前运行中反序列化的对象)。延迟的反序列化机制本身
。在运行时,延迟反序列化处理程序必须能够:a)确定要反序列化的代码对象,b)执行实际的反序列化,以及c)将序列化的代码对象附加到所有相关函数。
我们对前两点的解决方案是为快照添加一个新的专用内置区域,该区域可能只包含序列化的代码对象。序列化按照定义良好的顺序进行,每个代码对象的起始偏移量保存在内置快照区域的专用部分中。反向引用和散布的对象数据都是不允许的。
内置函数的延迟反序列化由DeserializeLazy built-in来处理,它在反序列化时安装在所有延迟内置函数上。在运行时调用时,它会对相关的 Code 对象进行反序列化,最后将其安装在 JSFunction (表示函数对象)和 SharedFunctionInfo(由相同函数文本创建的函数之间共享)上。每个内置函数最多只能反序列化一次。
除了内置函数之外,我们还为字节码处理程序实现了延迟反序列化。字节码处理程序是包含在 V8 的 Ignition 解释程序中执行每个字节码的逻辑的代码对象。与内置插件不同,它们既没有附加的 JSFunction 也没有 SharedFunctionInfo。相反,它们的代码对象直接存储在调度表中,解析器在调度到下一个字节码处理程序时将其索引。延迟反序列化与内置函数类似:DeserializeLazy 处理函数通过检查字节码数组,确定要反序列化的处理程序,反序列化代码对象,最后将反序列化的处理程序存储在调度表中。同样,每个处理程序最多只能反序列化一次。
验证结果
我们通过在 Android 设备上使用 Chrome 65 加载前 1000 个最受欢迎的网站(无论是否使用延迟反序列化)来评估内存节省。
平均而言,V8 的堆大小减少了 540KB,其中 25% 的受测站点节省了 620KB 以上,50%节省了 540KB 以上,75%节省了 420KB 以上。
运行时性能(以标准 JS 基准测量,如速度计,以及各种流行网站)均未受延迟反序列化的影响.
下一步
延迟反序列化确保每个 Isolate 只加载实际使用的内置代码对象。这已经是一个巨大的胜利,但我们相信可以更进一步,将每个 Isolate 的(内置相关)成本降低到零。
我们希望在今年晚些时候为您带来这方面的更新。敬请关注!