我们在 2012 年 5 月 31 日全面推出了 Windows 8 Release Preview 和
IE10 的第六个平台预览版。Windows 8 中包含一个 HTML5 浏览引擎,该引擎可增强 Metro 风格和桌面的浏览体验,同时改善使用 HTML5 和 JavaScript 编写的 Metro 风格应用程序的用户体验。发布预览版展现了 Chakra 的主要变化,Chakra 是我们首次在 IE9 中推出的现代 JavaScript 引擎。随着每一个平台预览版的推出,我们正一步步向我们的目标逼近,即为用户创建一个可在 Web 上提供卓越性能的引擎,同时确保该引擎高度兼容、可交互、且安全。本篇博文将介绍我们为了面向新兴 Web 应用程序场景提供卓越性能,而对 JavaScript 引擎所进行的功能增强。
实际 Web 应用程序的性能
Web 应用程序在近年来获得了迅猛的发展。十年前,Web 主要由包含静态内容的网站组成,类似于您在
博客、
小型的
企业登陆页面或
维基百科中所见到的情形。而 AJAX 的出现则推动了更为复杂、交互性更强的网站的快速发展,例如 Facebook 或 JetSetter。随后性能的不断提高为创建诸如
Office 365、
Bing 地图等大型复杂应用程序提供了可能性。最近,W3C 标准 API 的扩展、JavaScript 性能的提高,以及硬件获得加速的图形可让开发人员在 Web 上构建更为复杂的游戏,如
愤怒的小鸟、
海盗爱雏菊、
割绳子等。
随着应用程序的发展,性能因素将影响用户体验的变化。对于传统网站而言,初始页面的加载将决定用户需要多长时间才能看到内容。交互式网站和大型 Web 应用程序可能将受到 DOM 操作、CSS 处理和内存中大型内部状态操作效率的影响。HTML5 游戏通常依赖于快速的画布渲染、JavaScript 执行和高效垃圾回收。简言之,浏览器性能是一个复杂的问题,这通常需要考虑范围广泛且多种多样的应用程序。
在本篇博文中,我们仅将集中讨论一个浏览器子系统的性能,即 JavaScript 引擎。借助 JavaScript 性能最近所获得的提高,JavaScript 执行对于许多 Web 应用程序而言已不再是一个限制性因素。另一方面,随着性能的提高,许多新应用场景也纷纷出现,也为 JavaScript 引擎提出了其他要求。我们将继续找机会来变革 Chakra,以确保与 JavaScript 密集型的实际应用程序的性能要求相符合。
Web 应用程序性能的维度
Chakra 的内部架构
从 IE9 开始,Chakra JavaScript 的设计过程就一直围绕两项指导原则,而这两项原则的地位在 IE10 中依旧同样重要:
- 最大程度减少对用户体验中关键路劲的操作。这涉及推迟尽可能多的操作,直至迫不得已,从而避免共同操作,充分利用非活动闲置期,并行操作以最大程度减小对应用程序响应性的影响。
- 利用所有可用的硬件。也就是说需要使用所有可用的 CPU 内核,并生成高级专业化的 CPU 指令,例如在可用的情况下使用 Intel 的 SSE2。
Chakra 的并行架构
尽管 Chakra 只有一个浏览器子系统,但是其本身包含许多组件,这些组件可相互配合来共同处理和执行 JavaScript 代码。当浏览器下载 JavaScript 文件时,其将发送至 Chakra 的分析程序处理文件内容,并验证文件语法的正确性。这是应用至整个文件的唯一一项操作。后续步骤将在(包括全局函数在内的)各项函数中单独进行。由于系统即将执行某一函数(全局函数将在分析完成后立即运行),因此 Chakra 的分析程序将构建代码的抽象语法树 (AST) 表示,并将其传送至字节码生成器,该生成器将适于解释程序执行(而非 CPU 直接执行)的中间形式(字节码)。AST 和函数字节码将得以保留,因此无需在后续执行中重新创建它们。随后系统将调用解释程序来运行函数。解释程序在执行单独操作过程中将收集有关其遇到的输入类型的信息(配置文件),并跟踪函数调用的次数。
大量调用将达到某个阈值,解释程序将队列函数进行编译。与其它浏览器内的情形不同,Chakra 的实时 (JIT) 编译器将在单独专门的线程中运行,因此不会干涉脚本执行。编译器的唯一工作是为编译队列中的各个函数生成优化的计算机指令。单一函数编译完成,计算机代码的可用性信号将发送至主脚本线程。一旦开始下一次调用,函数的进入点就被重定向至新编译的计算机代码,并直接在 CPU 中执行操作。请注意系统将不会编译仅调用一次或两次的函数,以节省时间和资源。
JavaScript 是位于对开发人员隐藏的内存管理中的托管运行时,并且由自动垃圾回收器执行,该垃圾回收器将定期运行来清理不再使用的所有对象。Chakra 采用了一个传统、准代式的可进行标记与整理的垃圾回收器,其将在专门的线程中同时进行大部分的工作,以最大程度减少能中断用户体验的脚本执行暂停。
该体系结构可让 Chakra 几乎在页面加载期间就立即执行 JavaScript 代码。另一方面,在 JavaScript 活动密集期间,Chakra 可并行工作,并通过同时运行脚本、编译和回收垃圾而饱和使用多达三个 CPU 内核。
快速的页面加载时间
即便是相对静止的网站也倾向于使用 JavaScript 来进行交互、广告发布或社交共享。事实上,据
Steve Souders HTTP Archive 报道,包含于
Alexa 前一百万位页面中的 JavaScript 数量一直在稳步上升。
Alexa 前一百万位网页中 JavaScript 的使用数量
包含于这些网站中的 JavaScript 代码必须由浏览器的 JavaScript 引擎处理,而且每个脚本文件的全局函数必须在系统完全呈现内容之前执行。因此,最大程度减少对这一关键路径的操作数量将至关重要。Chakra 的分析程序和字节码解释程序的设计过程中充分考虑了这一目标。
字节码解释程序。在页面加载过程中所执行的 JavaScript 代码通常将进行初始化和仅执行一次的安装。为了最大程度减少整体页面的加载时间,系统必须立即开始执行这一代码,而不是等待实时编译器处理代码并发出计算机指令。解释程序将在代码翻译为字节码后立即开始运行 JavaScript 代码。为了进一步缩短首次执行指令的时间,Chakra 将仅为需要使用机制调用延迟分析执行的函数处理并发出字节码。
延迟的分析。
JSMeter 项目显示常见网页仅使用它们下载的代码,通常比例为 40-50%(参见右侧图表)。直观上看,这很有道理:开发人员通常将包含诸如
jQuery 或
dojo 等热门 JavaScript 库或类似常用于 Office 365 的自定义库,但却仅使用库所支持的功能。
为了优化这一应用场景,Chakra 将仅对源代码执行最基本的纯语法分析。其余工作(构建抽象语法树和生成字节码)将仅在函数将被调用时执行一项函数。该策略不仅将有助于在加载网页的过程中提高浏览器的响应性,同时还将节省内存占用空间。
IE9 中包含一个对 Chakra 延迟分析的限制。嵌套于其它函数内部的函数需要立即与外框函数一同分析。这一限制被证实十分重要,因为许多 JavaScript 库将采用所谓的“
现代模式”,其中大部分的库代码将包含于将立即执行的大型函数中。在 IE10 中,我们去除了这一限制,现在 Chakra 可延迟分析和不需要立即执行的所有函数的字节码生成。
JavaScript 密集型应用程序的性能改善
与之前的 IE9 类似,我们极力在 IE10 中改进实际 Web 应用程序的性能。然而,Web 应用程序在不同程度上依赖于 JavaScript 的性能。为了讨论 IE10 中的功能增强,我们有必要集中来看看 JavaScript 密集型的应用程序;在这些应用程序中,Chakra 的改进大幅提高了性能。另一类重要的 JavaScript 密集型应用程序包含 HTML5 游戏和模拟。
在 IE10 推出之初,我们分析了热门 JavaScript 游戏(例如
愤怒的小鸟、
割绳子或
坦克世界)的示例,以及模拟示例(例如
FishIE Tank、
HTML5 Fish Bowl、
Ball Pool、
Particle System)来理解哪些性能改进对用户体验将产生最大影响。我们的分析结果揭示出了大量常见特征和编码模式。所有应用程序都是由高频率计时器回调驱动的。这些应用程序中大部分都将使用画布进行渲染,但是也有一部分依赖于动画 DOM 元素,还有一些则使用二者的结合。在大多数应用程序中,至少有一部分代码是以面向对象的风格进行编写,或者是以应用程序代码,或者是以所包含的库(例如
Box2d.js)。短函数非常常见,也是读取和写入的常见属性,并具有多形性。所有应用程序都将执行浮点运算,而且许多应用程序将分配一部分内存,从而对垃圾回收器产生一定压力。这些常见的模式已经成为了我们在 IE10 性能改善工作的重点。以下部分将介绍我们为了回应这一目标而进行的更改。
重新设计并经过改进的实时编译器
IE10 中包含对 Chakra JIT 编译器的大量改进。我们对另外两个处理器结构体系新增了支持:x64 和 ARM。因此,无论用户是在 64 位的 PC 或是基于 ARM 的平板电脑上使用您的 JavaScript 应用程序,用户都可以享受到在 CPU 上直接执行所带来的好处。
我们该更改了生成计算机代码的基础方法。JavaScript 是一个非常动态的语言,因此这也对编译器了解代码生成的时间产生了一定的限制。例如,当编译以下函数时,编译器就无法知道所涉及对象的形状(属性布局)或其属性的类型。
function compute(v, w) {
return v.x + w.y;
}
在 IE9 中,Chakra 的编译器所生成的代码将在运行时定位所有属性,并处理所有合理的操作(在上述示例中:整数添加、浮点添加、或甚至字符串串联)。这些操作中的一部分将直接在计算机代码中进行处理,而其它则需要 Chakra 运行时的帮助。
在 IE10 中,JIT 编译器将产生基于配置文件、类型特定的计算机代码。换句话说,这将生成为特定形状对象或特定类型值定制的计算机代码。为了发出正确的代码,编译器需要知道输入值将收到的类型。由于 JavaScript 是一种动态的语言,因此源代码中不包含这一信息。我们增强了 Chakra 的解释程序来在运行时收集这些信息,我们将这一技术称为动态分析。当系统计划为某一函数进行 JIT 编译时,编译器将检查解释程序所收集运行时的配置文件,并发出为预期收入定制的代码。
解释程序将为其观察的运行收集信息,但是程序执行有可能导致运行时值偏离我们在所生成的优化代码中所进行的假设。对于所进行的各项假设,编译器都将发出一个运行时检查。如果后期执行产生了意外值,那么检查将失败,操作执行将救助指定计算机代码,解释程序将继续执行操作。救助(检查失败)的原因将得以记录,额外的配置文件信息将由解释程序收集,函数将由不同的假设进行重新编译。救助和重新编译是 IE10 中的两项基础的新功能。
所产生的净效果是 Chakra 的 IE10 编译器将为您的代码而生成更少的计算机指令,从而减少整体内存占用空间,并加速执行过程。这对于包含浮点算术和对象属性访问权限的应用程序影响尤为重大,例如我们此前曾讨论过的 HTML5 游戏和模拟。
如果您以面向对象的风格来编写 JavaScript 代码,那么您的代码还将从 Chakra 对函数内嵌的支持中受益。面向对象的代码通常包含大量相对较小的方法,相比这些函数的执行时间,这些函数调用的开销要小得多。函数内嵌可让 Chakra 节省这一开销,但是更为重要的是函数内嵌可大幅扩展其它传统编译器优化的范围,例如循环不变代码运动或复制传播。
更迅速的浮点算法
大多数 JavaScript 程序将执行一些整数算法。以下就列出了一个示例,即使是在重点并非算法的程序中,整数值通常被用作循环迭代变量或阵列索引。
function findString(s, a) {
for (var i = 0, al = a.length; i < al; i++) {
if (a[i] == s) return i;
}
return -1;
}
另一方面,浮点运算通常受限于某类应用程序,例如游戏、模拟、声音、图像或视频处理等。在过去,这些应用程序很少使用 JavaScript 编写,但是浏览器性能近来的发展也提高了 JavaScript 实施的可能性。在 IE9 中,我们对 Chakra 中更常见的整数运算进行了优化。在 IE10 中,我们大幅改进了浮点运算。
function compute(a, b, c, d) {
return (a + b) * (c − d);
}
从以上简单的函数中可以看出,JavaScript 编译器无法从源代码中确定参数 a、b、c、d 的类型。IE9 编译器将假设这些参数有可能成为整数,并产生更快的整数计算机指令。如果在执行过程中这些参数确实是整数,那么这将十分奏效。如果相反,系统使用了浮点数,那么代码将不得不依赖于 Chakra 运行时中速度更慢帮助程序函数。由于堆上中间值的装箱和取消装箱,函数调用的开销将进一步提高(在包括 Chakra 在内的大多数 32 位 JavaScript 引擎中,个别浮点值必须在堆中分配)。在上述表达式中,各项操作的结果需要一个堆分配,并紧跟堆中的存储值,然后从堆中检索进行下一操作的值。
在 IE10 中,编译器将利用解释程序所收集的配置文件信息来生成大幅加快的浮点代码。在上述示例中,如果配置文件表明所有参数都有可能成为浮点数,那么编译器将发出浮点计算机指令。整个表达式将在三个计算机指令中进行计算(假设所有参数都已经位于寄存器),所有中间值必须存储于寄存器,系统只需一个堆分配即可返回最终结果。
对于浮点密集型应用程序而言,这一方式将大幅提高其性能。实验表明 IE10 浮点操作的执行速度比 IE9 快 50%。此外,内存分配率的降低意味着垃圾回收器的数量也可减少。
更快的对象和属性访问
JavaScript 对象是分组逻辑相关值集的方便且广泛使用的机制。无论您是以面向结构化对象的编程风格使用 JavaScript 对象,或是仅将其作为获取值的灵活封装,您的代码都将从 IE10 中新增的对象分配和属性访问性能中受益匪浅。
正如我此前所提到的,高效属性访问使用 JavaScript 进行了编译,这是因为在编译过程中系统无法知道对象的形状。系统无需预定义的类型或类别,即可临时创建 JavaScript 对象。新属性能以运行顺序或任意顺序添加至对象(甚至从对象中删除)。因此,当编译以下方法时,编译器不知道从何处查找矢量对象的属性 x、y 和 z。
Vector.prototype.magnitude = function() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
在 IE9 中,我们引入了内联缓存来大幅加速对属性的访问。内联缓存可记住对象的形状,以及对象内存中给定属性的位置。内联缓存可仅记住一个对象位置,而且如果所有对象函数的形状相同,那么内联缓存的运行效果将非常好。在 IE10 中,我们添加了一个辅助缓存机制,该机制提高了不同形状(多态)对象上代码操作的性能。
在系统读取属性值之前,编译器必须验证该对象的形状是否与存储于内联缓存中的对象形状相匹配。为进行这一操作,在 IE9 中,编译器将在各个属性访问前生成一个运行时形状检查。由于程序通常将连续读取或写入相同对象的多个属性(如以下示例),因此所有这些检查都将增加开销。
function collide(b1, b2) {
var dx = b1.x - b2.x;
var dy = b1.y - b2.y;
var dvx = b1.vx - b2.vx;
var dvy = b1.vy - b2.vy;
var distanceSquare = (dx * dx + dy * dy) || 1.0;
//...
}
在 IE10 中,Chakra 将生成专为预期对象形状而定制的代码。通过准确的符号跟踪,并结合救助和重新编译功能,新编译器可大幅减少进行运行时形状检查的数量。上述示例并没有分别进行 8 项形状检查,而只是分别对 b1 和 b2 进行了两项检查。此外,一旦对象的形状得到确定,所有属性的位置也将确定,C++ 的读取或写入工作便可高效完成。
在 ECMAScript 5 中,对象可能包含一类名为取值函数属性的新功能。取值函数属性与传统数据属性有所不同,主要体现在自定义获取和设置的函数将被调用,以处理读取和写入操作。读取函数属性是一个方便的机制,可用于添加数据封装、计算属性、数据验证或更改通知。Chakra 的内部类型系统和内联缓存旨在调节取值函数的属性,并提高读取和写入其它值的效率。
如果您编写了一个 HTML56 或游戏,那么您通常需要一个物理引擎来执行在重力作用下制作对象实际移动、模拟碰撞等操作所需的计算。对于非常简单的物理特性,您可构建您自己的引擎,但是对于更加复杂的要求,您通常需要使用一个 JavaScript 中可用的、热门的物理库,例如(从
Box2d 中传送的)
Box2d.js。这些库通常使用较小的对象,例如点、矢量或颜色。在各个动画帧中,系统将创建并立即放弃这些对象中的大部分。因此,JavaScript 运行时高效创建对象将十分重要。
var Vector = function(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
Vector.prototype = {
//...
normalize : function() {
var m = Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
return new Vector(this.x / m, this.y / m, this.z / m);
},
add : function(v, w) {
return new Vector(w.x + v.x, w.y + v.y, w.z + v.z);
},
cross : function(v, w) {
return new Vector(-v.z * w.y + v.y * w.z, v.z * w.x - v.x * w.z, -v.y * w.x + v.x * w.y);
},
//...
}
在 IE10 中,JavaScript 对象的内部布局已得到优化,因而简化了对象的创建过程。在 IE9 中,各个对象包含一个尺寸固定的标头和一个可扩展的属性阵列。系统可能需要在创建对象后添加一些额外的属性,为了调节这些属性,必须使用可扩展的属性阵列。并非所有 JavaScript 应用程序拥有这一灵活性,而且对象通常将在构造过程中收到其大部分属性。这一特征可让 Chakra 直接与标头为这类对象分配其大部分属性,因而导致只所有新创建的对象分配一个内存(而非两个)。这一变化同时减少了读取或写入对象属性所需的内存延迟数量,并改善了寄存器的使用情况。经改进的对象布局和更少的运行时形状检查将让属性访问的速度提高多达 50%。
垃圾回收的功能增强
正如我们此前所讨论的,HTML5 游戏和动画通常将以很高的速率创建并放弃对象。JavaScript 程序不会显式破坏对象并回收内存。相反,它们依赖于引擎的垃圾回收器来定期回收由未使用的对象占用的内存,从而为新对象腾出空间。自动垃圾回收可简化编程过程,但是通常需要 JavaScript 执行来不时暂停运行,以让回收器进行其操作。如果回收器运行时间过长,那么整个浏览器将变得没有响应。在 HTML5 游戏中,即便是短暂的暂停(十分之一毫秒)都将产生中断,这是因为用户将在动画故障中感受得到这一中断。
在 IE10 中,我们对内存分配器和垃圾回收器进行了大量功能增强。我们已经讨论了对象布局的变更和浮点算术特定的计算机代码的生成,这些内容将减少内存分配。此外,Chakra 现在将从单独的内存空间中分配叶对象(例如数字和字符串)。叶对象并不拥有对其它对象的指针,因此我们不需要在垃圾回收过程中对其给予像常规对象一样多的关注。从单独空间中分配叶对象拥有两大优势。首先,整个空间可在标记阶段跳过,因而缩短整个持续时间。第二,在并发收集过程中,从叶对象空间进行的新分配不需要重新扫描受影响的页面。由于 Chakra 的回收器可与主脚本线程同时运行,运行的脚本可修改或在已经处理过的页面上创建新对象。为了确保该对象不被过早的收集,Chakra 写入保护页面将在标记阶段开始前启动。已经在标记阶段被写入的页面稍后必须在主脚本线程中重新扫描。由于叶对象不需要该操作,叶对象空间的页面亦不需要写入保护,或稍后进行重新扫描。这将节省主脚本线程的宝贵时间,并减少暂停。HTML5 游戏和动画将从这一变化中受益匪浅,这是因为它们通常过度使用浮点数,并将大部分分配的内存用于堆装箱数。
当用户与 Web 应用程序直接交互时,系统应尽可能迅速地执行应用程序的代码,理想情况下应不发生垃圾回收的中断。然而,当用户从浏览器中切换离开,或只是更换选项卡时,减少当前不活动的网站或应用程序的内存的占用十分重要。因此在 IE9 中,当系统分配了足够的内存时,Chakra 将触发对现有 JavaScript 代码的收集。这对于大多数应用程序十分奏效,但是这对于某些由高频率计时器驱动的应用程序(例如 HTML5 游戏和动画)将产生一定问题。对于这些应用程序而言,回收将频繁触发,并导致帧数减少,以及整体用户体验的下降。这一问题这可能在
坦克世界游戏中表现得最为明显,但是其它 HTML5 模拟同样也将表现出频繁垃圾回收所导致的动画暂停。
在 IE10 中,我们通过将垃圾回收与浏览器的剩余部分进行协调,而解决了这一问题。现在,Chakra 可将垃圾回收延迟至脚本执行结束时,并需要在脚本停止活动间隔后从浏览器回调。如果间隔期间未执行任何脚本,Chakra 将启动回收,否则回收将延迟。这一技术可让我们在浏览器(或其中一个选项卡)停止活动时,节省内存占用,同时大幅降低由动画驱动的应用程序中的回收频率。
这些变化相互结合,共同缩短了用于主线程上垃圾回收的时间,平均减少了所测量的 HTML5 模拟四项因素中的一项。作为 JavaScript 执行时间中的一部分,垃圾回收从约 27% 下降到了约 6%。
总结
IE10 面向 JavaScript 密集型应用程序,特别是 HTML5 游戏和模拟大幅提高了性能。这些性能的提高得益于 Chakra 中的大量重要改进:从 JIT 编译器的新基础功能到垃圾回收器的变更。
随着我们对 IE10 开发的圆满成功,我们热烈庆祝所获得的改进,但是我们也清醒地认识到性能改进是一个永恒的追求。现在,几乎每天都有新应用程序问世,这些应用程序在不断测试这现代浏览器及其 JavaScript 引擎的受限范围。毫无疑问,我们还需要在下一个版本中进行大量改进工作!
如果您是一名 JavaScript 开发人员,那么我们热切地希望听到您的反馈。如果 IE10 的新功能和性能改进有助于您为用户创建一个全新的使用体验,或改善现有应用程序,那么请与我们分享。如果您正面临 IE 中任何性能限制,也请与我们联系。我们将认真阅读本篇博文的所有评论,并尽最大努力来改善 IE10 和 Windows 8,让其成为最全方位、性能最佳的可用应用程序平台。
—Andrew Miadowicz,JavaScript 项目经理
标签:IE相关性能JavaScriptIE10IE Web