TraceMonkey 概述

这篇文章由 David Mandelin 撰写,他是 Mozilla JavaScript 团队的成员。

Firefox 3.5 拥有一个全新的 JavaScript 引擎 TraceMonkey,它运行许多 JavaScript 程序的速度比 Firefox 3 快 3-4 倍,从而加快了现有 Web 应用程序的速度,并支持新的应用程序。本文将深入了解 TraceMonkey 的主要部分及其加速 JS 的方式。本文还将解释哪些类型的程序可以从 TraceMonkey 中获得最佳加速,以及您可以采取哪些措施来加速程序的运行。

为什么快速运行 JS 很困难:动态类型

像 JavaScript 和 Python 这样的高级动态语言使编程更加高效,但它们一直以来都比 Java 或 C 等静态类型语言慢。一条经验法则是,JS 程序可能比等效的 Java 程序慢 10 倍。

JS 和其他动态脚本语言通常比 Java 或 C 运行得慢的主要原因有两个。第一个原因是,在动态语言中,通常无法提前确定值的类型。因此,语言必须以通用格式存储所有值,并使用通用操作处理值。

相比之下,在 Java 中,程序员会为变量和方法声明类型,因此编译器可以提前确定值的类型。然后,编译器可以生成使用专用格式和操作的代码,这些格式和操作比通用操作运行得快得多。我将这些操作称为 **类型专用** 操作。

动态语言运行速度较慢的第二个主要原因是,脚本语言通常用解释器实现,而静态类型语言则编译成本地代码。解释器更容易创建,但它们会为跟踪其内部状态带来额外的运行时开销。像 Java 这样的语言编译成机器语言,几乎不需要状态跟踪开销。

让我们用一张图片来具体说明一下。以下是针对一个简单的数字加法操作 a + b 的速度降低情况,其中 ab 是整数。现在,忽略最右边的条形图,重点关注 Firefox 3 JavaScript 解释器与 Java JIT 的比较。每一列都显示了在每种编程语言中完成加法操作所需执行的步骤。时间自上而下,每个框的高度与其完成框中步骤所需的时间成正比。

time diagram of add operation

在中间,Java 只执行一条机器语言加法指令,该指令在时间 T(一个处理器周期)内运行。由于 Java 编译器知道操作数是标准机器整数,因此它可以使用标准的整数加法机器语言指令。就这些了。

在左边,SpiderMonkey(FF3 中的 JS 解释器)大约需要 40 倍的时间。棕色框代表解释器开销:解释器必须读取加法操作并跳转到解释器的代码以进行通用加法。橙色框代表由于解释器不知道操作数类型而必须完成的额外工作。解释器必须解包 ai 的通用表示,找出它们的类型,选择特定的加法操作,将值转换为正确的类型,最后将结果转换回通用格式。

该图表明,使用解释器而不是编译器会略微降低速度,但缺乏类型信息会大大降低速度。如果我们希望 JS 的运行速度比 FF3 快一点,根据 Amdahl 定律,我们需要针对类型做点什么。

通过跟踪获取类型

我们在 TraceMonkey 中的目标是编译类型专用代码。为此,TraceMonkey 需要知道变量的类型。但 JavaScript 没有类型声明,我们之前也说过,JS 引擎实际上不可能提前找出类型。因此,如果我们想要提前编译所有内容,那就没辙了。

所以让我们换个角度思考这个问题。如果我们让程序在解释器中运行一段时间,引擎就可以直接 *观察* 值的类型。然后,引擎可以使用这些类型来编译快速类型专用代码。最后,引擎可以开始运行类型专用代码,并且运行速度会快得多。

这个想法中有一些关键细节。首先,当程序运行时,即使存在许多 if 语句和其他分支,程序始终只沿一个方向运行。因此,引擎不会观察整个方法的类型——引擎会通过程序实际采用的路径(我们称之为 *跟踪*)来观察类型。因此,虽然标准编译器会编译方法,但 TraceMonkey 会编译跟踪。每次跟踪编译的一项附带好处是,在跟踪中发生的函数调用会内联,从而使跟踪的函数调用非常快。

其次,编译类型专用代码需要时间。如果一段代码只运行一次或几次,这在 Web 代码中很常见,则编译和运行这段代码可能比仅仅在解释器中运行这段代码花费更多时间。因此,只有编译 *热点代码*(执行多次的代码)才是值得的。在 TraceMonkey 中,我们通过只跟踪循环来实现这一点。TraceMonkey 最初在解释器中运行所有内容,并在循环变热(运行超过几次)时开始记录该循环的跟踪。

只跟踪热点循环有一个重要的后果:只运行几次的代码不会在 TraceMonkey 中加速。请注意,这在实践中通常无关紧要,因为只运行几次的代码通常运行速度太快,以至于不会被注意到。另一个后果是,完全没有被采用的循环路径永远不需要编译,从而节省了编译时间。

最后,我们之前说过,TraceMonkey 通过观察执行来找出值的类型,但众所周知,过去的性能不能保证未来的结果:下次运行代码时类型可能不同,或者第 500 次运行时类型可能不同。如果我们尝试运行为数字编译的代码,而这些值实际上是字符串,就会发生非常糟糕的事情。因此,TraceMonkey 必须在编译的代码中插入类型检查。如果检查不通过,TraceMonkey 必须离开当前跟踪并为新类型编译新的跟踪。这意味着,具有许多分支或类型更改的代码在 TraceMonkey 中运行速度会略慢一些,因为它需要时间来编译额外的跟踪并在它们之间跳转。

TraceMonkey 在行动

现在,我们将通过一个示例程序来展示跟踪在行动中的情况,该程序将前 N 个整数加到一个起始值中

 function addTo(a, n) {
   for (var i = 0; i < n; ++i)
     a = a + i;
   return a;
 }

 var t0 = new Date();
 var n = addTo(0, 10000000);
 print(n);
 print(new Date() - t0);

TraceMonkey 始终在 *解释器* 中开始运行程序。每次程序开始循环迭代时,TraceMonkey 会短暂进入 *监控* 模式,以增加该循环的计数器。在 FF3.5 中,当计数器达到 2 时,该循环被认为是热的,此时需要进行跟踪。

现在,TraceMonkey 在解释器中继续运行,但开始在代码运行时 *记录* 跟踪。跟踪只是运行到循环结束的代码,以及使用的类型。类型是通过查看实际值来确定的。在我们的示例中,循环执行以下 JavaScript 语句序列,该序列成为我们的跟踪

    a = a + i;    // a is an integer number (0 before, 1 after)
    ++i;          // i is an integer number (1 before, 2 after)
    if (!(i < n)) // n is an integer number (10000000)
      break;

这就是跟踪在类似 JavaScript 的表示法中的样子。但 TraceMonkey 需要更多信息才能编译跟踪。真正的跟踪看起来更像这样

  trace_1_start:
    ++i;            // i is an integer number (0 before, 1 after)
    temp = a + i;   // a is an integer number (1 before, 2 after)
    if (lastOperationOverflowed())
      exit_trace(OVERFLOWED);
    a = temp;
    if (!(i < n))   // n is an integer number (10000000)
      exit_trace(BRANCHED);
    goto trace_1_start;

此跟踪表示一个循环,它应该作为循环进行编译,因此我们使用 goto 来直接表达这一点。此外,整数加法可能会溢出,这需要特殊处理(例如,用浮点加法重新执行),而这反过来又需要退出跟踪。因此,跟踪必须包含一个溢出检查。最后,如果循环条件为假,跟踪将以相同的方式退出。退出代码告诉 TraceMonkey 退出跟踪的原因,以便 TraceMonkey 可以决定下一步该怎么做(例如,是否重做加法或退出循环)。请注意,跟踪以特殊的内部格式记录,该格式从未公开给程序员——上面使用的表示法只是为了说明目的。

记录完跟踪后,就可以将其 *编译* 成类型专用的机器代码。这种编译由一个小型 JIT 编译器(恰如其分地命名为 *nanojit*)执行,结果存储在内存中,准备由 CPU 执行。

下次解释器通过循环头时,TraceMonkey 将开始 *执行* 编译后的跟踪。现在,程序运行速度非常快。

在第 65537 次迭代时,整数加法将溢出。(2147450880 + 65537 = 2147516417,大于 2^31-1 = 2147483647,这是最大的有符号 32 位整数)。此时,跟踪将以 OVERFLOWED 代码退出。看到这一点,TraceMonkey 将返回解释器模式并重新执行加法。由于解释器会通用地执行所有操作,因此加法溢出将得到处理,一切正常。TraceMonkey 还将开始监控此退出点,如果溢出退出点曾经变热,将从该点开始新的跟踪。

但在这个特定程序中,实际发生的事情是,程序再次通过循环头。TraceMonkey 知道它在这个点有一个跟踪,但 TraceMonkey 也知道它不能使用该跟踪,因为该跟踪是针对整数值的,但 a 现在是浮点格式。因此,TraceMonkey 会记录一个新的跟踪

  trace_2_start:
    ++i;            // i is an integer number
    a = a + i;      // a is a floating-point number
    if (!(i < n))   // n is an integer number (10000000)
      exit_trace(BRANCHED);
    goto trace_2_start;

然后,TraceMonkey 会编译新的跟踪,并在下一次循环迭代时开始执行它。这样,即使类型发生变化,TraceMonkey 也会使 JavaScript 以机器代码形式运行。最终,跟踪将以 BRANCHED 代码退出。此时,TraceMonkey 将返回解释器,解释器接管并完成程序的运行。

以下是该程序在我笔记本电脑(2.2GHz MacBook Pro)上的运行时间

系统 运行时间(毫秒)
SpiderMonkey (FF3) 990
TraceMonkey (FF3.5) 45
Java (使用 int) 25
Java (使用 double) 74
C (使用 int) 5
C (使用 double) 15

该程序从跟踪中获得了巨大的 22 倍加速,并且运行速度与 Java 相当!在循环中执行简单算术运算的函数通常会从跟踪中获得很大的加速。许多 SunSpider 基准测试的位操作和数学运算,例如 bitops-3bit-bits-in-bytectrypto-sha1math-spectral-norm,获得了 6 倍到 22 倍的加速。

使用更复杂操作的函数,例如对象操作,速度提升幅度较小,通常在 2-5 倍之间。这从阿姆达尔定律和复杂操作耗时较长的事实可以数学推导出来。回顾时间图,考虑一个更复杂的操作,其绿色部分耗时为 30T。橙色和棕色部分的总时长仍然约为 30T,因此消除它们可以带来 2 倍的提速。SunSpider 基准测试中的 string-fasta 就是这种程序的示例:大部分时间都花在了字符串操作上,而这些操作的绿色框部分耗时相对较长。

以下是在浏览器中可以尝试的示例程序版本。

数值结果:

运行时间:

平均运行时间:

理解和解决性能问题

我们的目标是使 TraceMonkey 始终足够快,以便您能够以最能表达您想法的方式编写代码,而不必担心性能。如果 TraceMonkey 没有加速您的程序,我们希望您能将其报告为错误,以便我们改进引擎。当然,您可能需要您的程序在当今的 FF3.5 中运行得更快。在本节中,我们将解释一些工具和技术,用于解决在启用跟踪 JIT 后没有获得良好(2 倍或更多)提速的程序的性能问题。(您可以通过转到 **about:config** 并将首选项 **javascript.options.jit.content** 设置为 false 来禁用 JIT。)

第一步是理解问题的原因。最常见的原因是 *跟踪中止*,这仅仅意味着 TraceMonkey 无法完成跟踪记录,并且放弃了。通常的结果是包含中止的循环将在解释器中运行,因此您不会在该循环上获得提速。有时,循环中的一条路径会被跟踪,但在另一条路径上会发生中止,这会导致 TraceMonkey 在解释和运行本地代码之间来回切换。这可能会导致提速减少、没有提速,甚至变慢:切换模式需要时间,因此快速切换会导致性能低下。

使用浏览器的调试版本或 JS shell(我自己 构建 的 - 我们不发布这些版本),您可以通过设置 TMFLAGS 环境变量来指示 TraceMonkey 打印有关中止的信息。我通常是这样做的

TMFLAGS=minimal,abort
dist/MinefieldDebug.app/Contents/MacOS/firefox

minimal 选项打印出所有开始记录和成功完成记录的位置。这提供了有关跟踪器尝试执行的操作的基本了解。abort 选项打印出所有由于不支持的结构而导致记录中止的位置。(设置 TMFLAGS=help 将打印其他 TMFLAGS 选项列表,然后退出。)

(还要注意,TMFLAGS 是打印调试信息的最新方法。如果您使用的是 FF3.5 版本的调试版本,则环境变量设置是 TRACEMONKEY=abort。)

以下是一个在 TraceMonkey 中没有获得太多提速的示例程序。

function runExample2() {
  var t0 = new Date;

  var sum = 0;
  for (var i = 0; i < 100000; ++i) {
    sum += i;
  }

  var prod = 1;
  for (var i = 1; i < 100000; ++i) {
    eval("prod *= i");
  }
  var dt = new Date - t0;
  document.getElementById(example2_time').innerHTML = dt + ' ms';
}

运行时间:

如果我们设置 TMFLAGS=minimal,abort,我们将得到以下内容

Recording starting from ab.js:5@23
recording completed at  ab.js:5@23 via closeLoop

Recording starting from ab.js:5@23
recording completed at  ab.js:5@23 via closeLoop

Recording starting from ab.js:10@63
Abort recording of tree ab.js:10@63 at ab.js:11@70: eval.

Recording starting from ab.js:10@63
Abort recording of tree ab.js:10@63 at ab.js:11@70: eval.

Recording starting from ab.js:10@63
Abort recording of tree ab.js:10@63 at ab.js:11@70: eval.

前两对行显示第一个循环(从第 5 行开始)跟踪良好。后面的行显示 TraceMonkey 开始跟踪第 10 行的循环,但每次都因 eval 而失败。

关于此调试输出的一个重要说明是,您通常会看到一些消息提到 *内部树* 正在增长、稳定等。这些实际上不是问题:它们通常只表示由于 TraceMonkey 连接内部和外部循环的方式而导致完成跟踪循环的 *延迟*。事实上,如果您在这些中止之后进一步查看输出,您通常会看到循环最终确实被跟踪了。

否则,中止主要由跟踪尚不支持的 JavaScript 结构导致。对于像 + 这样的基本操作,跟踪记录过程比对于像 arguments 这样的高级功能更容易实现。我们在 FF3.5 版本发布之前没有时间对每个 JavaScript 功能进行健壮、安全的跟踪,因此在 FF3.5.0 中,一些更高级的功能(如 arguments)没有被跟踪。其他没有被跟踪的高级功能包括 getter 和 setter、with 和 eval。对闭包的支持是部分的,具体取决于它们的使用方式。重构以避免这些结构可以提高性能。

有两个特别重要的 JavaScript 功能没有被跟踪,它们是

  • 递归。TraceMonkey 不将通过递归发生的重复视为循环,因此不会尝试跟踪它。重构为使用显式的 forwhile 循环通常会带来更好的性能。
  • 获取或设置 DOM 属性。(DOM 方法调用是可以的。)通常不可能避免这些结构,但是将 DOM 属性访问移出热循环和性能关键部分的代码重构应该有所帮助。

我们正在积极地努力跟踪上面提到的所有功能。例如,对跟踪 arguments 的支持已经在夜间构建版本中可用。

以下是被重构为避免 eval 的慢速示例程序。当然,我可以简单地内联进行乘法运算。相反,我使用由 eval 创建的函数,因为这是重构 eval 的更通用方法。请注意,eval 仍然无法被跟踪,但它只运行一次,因此并不重要。

function runExample3() {
  var t0 = new Date;

  var sum = 0;
  for (var i = 0; i < 100000; ++i) {
    sum += i;
  }

  var prod = 1;
  var mul = eval("(function(i) { return prod * i; })");

  for (var i = 1; i < 100000; ++i) {
    prod = mul(i);
  }
  var dt = new Date - t0;
  document.getElementById('example3_time').innerHTML = dt + ' ms';
}

运行时间:

还有一些更加晦涩的场景也会影响跟踪性能。其中一个是 *跟踪爆炸*,它发生在循环有许多路径通过它时。考虑一个循环,其中有 10 个 if 语句排成一行:该循环有 1024 条路径,可能导致记录 1024 条跟踪。这将占用太多内存,因此 TraceMonkey 将每个循环限制为 32 条跟踪。如果循环少于 32 条热跟踪,它将表现良好。但是,如果每个路径的出现频率相同,那么只有 3% 的路径会被跟踪,性能就会下降。

这种问题最好用 TraceVis 来分析,TraceVis 会创建 TraceMonkey 性能的可视化视图。目前,构建系统只支持为 shell 构建启用 TraceVis,但基本系统也可以在浏览器中运行,并且有 正在进行的工作 在浏览器中以方便的形式启用 TraceVis。

关于 TraceVis 的 博客文章 目前是对图表含义以及如何使用它们诊断性能问题的最详细解释。这篇文章还包含对图表的详细分析,这有助于理解 TraceMonkey 的总体工作原理。

比较性 JIT 文献

在这里,我将对其他 JavaScript JIT 设计进行一些比较。我将更多地关注假设设计,而不是竞争引擎,因为我对它们的详细信息并不了解——我已经阅读了发布信息,并略读了一些代码。另一个重大警告是,实际性能至少与工程细节一样取决于引擎架构。

一种设计选择可以称为 *每方法非专门化 JIT*。我的意思是,一个一次编译一个方法并生成通用代码的 JIT 编译器,就像解释器所做的那样。因此,我们图表中的棕色框被裁掉了。这种 JIT 不需要花费时间来记录和编译跟踪,但它也不进行类型专门化,因此橙色框仍然存在。这种引擎仍然可以通过精心设计和优化橙色框代码来获得相当快的速度。但是,在该设计中,无法完全消除橙色框,因此数字程序的最高性能不会像类型专门化引擎那样好。

据我所知,截至本文撰写之时,Nitro 和 V8 都是轻量级非专门化 JIT。(我听说 V8 确实尝试通过查看源代码来推测一些类型(例如,推测在 a >> 2a 是一个整数),以便进行一些类型专门化。)看来 TraceMonkey 在数字基准测试中通常更快,如上所述。但是,TraceMonkey 在使用更多对象的基准测试中表现不佳,因为我们的对象操作和内存管理尚未优化得那么好。

基本 JIT 的进一步发展是 *每方法类型专门化 JIT*。这种 JIT 尝试根据调用方法时的参数类型来对方法进行类型专门化。与 TraceMonkey 一样,这需要一些运行时观察:基本设计在每次调用方法时都会检查参数类型,如果这些类型以前没有被看到过,则会编译方法的新版本。与 TraceMonkey 一样,这种设计可以对代码进行高度专门化,并删除棕色框和橙色框。

据我所知,还没有人将每方法类型专门化 JIT 用于 JavaScript,但我不会感到惊讶,如果有人正在研究它。

与跟踪 JIT 相比,每方法类型专门化 JIT 的主要缺点是,基本每方法 JIT 只能直接观察方法的输入类型。它必须尝试通过算法推断方法内部变量的类型,这对 JavaScript 来说很困难,尤其是如果方法读取对象属性。因此,我认为每方法类型专门化 JIT 必须对方法的某些部分使用通用操作。每方法设计的最大优势是,方法只需要针对每组输入类型编译一次,因此它不会受到跟踪爆炸的影响。反过来,我认为每方法 JIT 在路径较多的方法上往往更快,而跟踪 JIT 在高度类型专门化的方法上往往更快,尤其是在方法还读取许多属性值的情况下。

结论

到目前为止,您应该对什么使 JavaScript 引擎变快、TraceMonkey 如何工作以及如何分析和解决在 TraceMonkey 下运行 JavaScript 时可能出现的某些性能问题有了很好的了解。如果您遇到任何重大性能问题,请报告错误。错误报告也是我们提供额外调整建议的好地方。最后,我们一直在努力改进,因此如果您喜欢使用最新版本,请查看 TraceMonkey 夜间构建版本。


68 条评论

  1. Gijs

    精彩的文章!读完后我有两个问题(部分基于我在 3.5 发布前听到的一些消息),希望这些问题的答案也能对其他人有所帮助。

    首先,在 Firefox 3.5 中,是否对 chrome(附加组件/扩展)代码启用了跟踪?

    其次,DOM 和/或 XPCOM(/XPConnect)调用如何影响跟踪?它们是否强制使用解释器,或者是否可以跟踪包含此类调用的循环?如果这些调用通常不会被跟踪,有没有简单的方法来提高包含这些调用的循环的性能?据我所知,使用你在 eval() 示例中使用的迷你函数将无济于事,因为 DOM/XPCOM/XPConnect 调用仍然不会被跟踪(因此代码会切换回解释器模式),对吗?

    2009 年 7 月 17 日 上午 6:39

  2. baka_toroi

    优秀文章!对编程经验较少的人来说,文章非常详细且易于理解。

    2009 年 7 月 17 日 上午 6:56

  3. Klaus Paiva

    精彩的解释!非常有用。

    2009 年 7 月 17 日 上午 7:48

  4. […] TraceMonkey 概述 […]

    2009 年 7 月 17 日 上午 8:35

  5. Sean Coates

    好奇:为什么如果我反复点击“运行示例 3”按钮,数字会增加?(我很快就让它上升到超过 250 毫秒。)

    在 Safari 中不会发生这种情况,如果我让按钮保持一段时间(比如 10 秒),它会降回 2 毫秒。

    2009 年 7 月 17 日 上午 8:53

  6. Boris

    Gijs,这篇文章实际上涵盖了这一点。DOM 获取器和设置器会强制使用解释器。DOM 方法调用不会(至少常见的方法不会)。

    2009 年 7 月 17 日 上午 9:27

  7. Gijs

    Boris:说得好,我不知道为什么我在评论时没想起这一点。但我仍然想知道有关 XPCOM 的情况!:-)

    2009 年 7 月 17 日 上午 9:45

  8. Christopher Blizzard

    @Sean – 我们正在调查此事。不应该发生这种情况,我已经创建了一个错误报告。感谢您指出这一点!(另外,如果您一直点击,最终它会降到零,这是一种缓存问题吗?)

    2009 年 7 月 17 日 上午 9:47

  9. Boris

    Gijs:DOM 东西是 XPCOM,对吧?基本上,在 js/src/xpconnect/src/dom_quickstubs.qsconf 中列出的任何接口/方法都将被跟踪。

    2009 年 7 月 17 日 上午 9:56

  10. Ian Stirling

    与早期版本的差异更加惊人。

    例如(凭记忆)

    第一个测试 - FF1.5 - 17000 毫秒,FF3 - 2500 毫秒,FF3.5.1 - 70 毫秒

    速度提高了 300 倍。

    2009 年 7 月 17 日 上午 11:43

  11. William Edney

    David -

    很棒的文章和解释!谢谢!

    您说到

    “但是,TraceMonkey 在使用更多对象的基准测试中表现不佳,因为我们的对象操作和内存管理尚未经过充分优化。”

    是否计划改进 TraceMonkey 中的这些操作?

    我工作的代码使用了非常多的对象,我发现 V8 和 Nitro 在代码的很多部分比 TraceMonkey(FF 3.5 最终版本)快 2 倍。

    只是好奇...

    干杯,

    - Bill

    2009 年 7 月 17 日 下午 12:59

  12. David Stokar

    @Boris:没错 - 但进行远程操作会使加速两倍的努力失效。

    2009 年 7 月 17 日 下午 1:12

  13. Boris

    Bill,确实有一些工作正在进行。如果您能提交一个错误报告并附上您的代码,这是确保我们调查代码中任何特定性能问题的最佳方式。请在错误报告中 CC “:bz”。

    2009 年 7 月 17 日 下午 1:19

  14. Untraced Monkey

    阅读完评论后,我尝试反复点击示例 3,我注意到了 Sean 报告的相同问题。时间缓慢增加,然后突然跃升到大约 240 毫秒。同时,Firefox 的内存使用量也突然增加(如 Windows 任务管理器中显示)。当我继续点击时,内存使用量最终会降回原来的水平,执行时间会降回大约 2 毫秒(对我来说,它没有降到零)。也许这将有助于您找到错误。

    2009 年 7 月 17 日 下午 1:26

  15. Robert

    运行测试 3 时,第一次调用返回的时间为 1 毫秒。第二次(或更晚)调用返回 2 毫秒。后续调用将此时间增加到 10-15 毫秒,之后时间会升级到大约 200-250 毫秒。过了一段时间(基于调用次数,而不是经过时间),执行时间会立即降回到 2 毫秒,并且循环重复。

    关于为什么相同的调用如果我调用足够多次会慢 200 倍,您有什么见解吗?

    2009 年 7 月 17 日 下午 2:42

  16. Jeff Walden

    关于重构:请记住,随着我们改进 TraceMonkey 以跟踪越来越多的构造,重构可能会随着时间的推移变得不必要,因此,仔细微优化可能不值得付出努力。此外,请记住,所有 JavaScript 引擎的优化方式都不同,并且知道解决方法可能会以不同的方式影响不同的引擎。

    同样,关于这一点,对象创建是当前正在加速的操作之一,除了文章中提到的操作之外。

    2009 年 7 月 17 日 下午 3:27

  17. Ivan Enderlin

    嘿 :-),

    感谢您这篇引人入胜的文章。真的很棒!

    我刚刚注意到一个错别字。在图表后的第二段中,您写道:“解释器必须解包 a 和 i 的通用表示。”也许应该是“a 和 b”,对吗?

    再次感谢 :-)。

    2009 年 7 月 17 日 下午 3:46

  18. Ron

    很棒的文章!我很乐意看到更多像这样的幕后文章,以及有关 TraceMonkey 发展状况的更新。

    另外,您的 Java 时间完全不正常。现代 JVM 的执行时间与 C 非常接近(实际上在某些情况下由于动态分析,它们可能比 C 更快)。在进行微基准测试时,有一些陷阱会导致时间偏差。最大的两个通常是:1)没有执行足够多次的代码以让动态分析器启动,以及 2)将(全部或部分)微基准测试放在 main() 中,因为 main 从未进行 JIT 编译并且总是运行解释器。此外,运行 -server JVM 会导致速度大幅提升,因为 Java 会进行更积极的优化。

    我运行了 3.0 和 3.5 测试,以获得我的计算机性能相对于您的计算机性能的基线(我的速度慢 40% :-P),然后在 Java 中运行了相同的基准测试(使用 1.6.0_14)。

    时间(第一个数字是我的计算机,第二个数字是 Java 时间是我的计算机的预测)
    SpiderMonkey(FF3):1662
    TraceMonkey(FF3.5):75
    Java(使用 int):17,10.2
    Java(使用 int) -server:7,4.2
    Java(使用 double):36,21.6
    Java(使用 double) -server:25,15

    如您所见,我对您的计算机上的服务器 JVM 时间的预测将是 4.2 和 15,正好与您为 C 设置的 5 和 15 一致。

    这是我的代码。要运行 double 测试,只需在 main() 中将 runIntTest() 更改为 runDblTest()。

    public class Test {
    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
    runIntTest();
    System.out.println();
    }
    }
    private static void runIntTest() {
    long t0 = System.currentTimeMillis();
    int n = addTo(0, 10000000);
    System.out.println(n);
    System.out.println(System.currentTimeMillis() – t0);
    }
    private static void runDblTest() {
    long t0 = System.currentTimeMillis();
    double n = addTo(0.0, 10000000);
    System.out.println(n);
    System.out.println(System.currentTimeMillis() – t0);
    }
    private static int addTo(int a, int n) {
    for (int i = 0; i < n; ++i) a = a + i;
    return a;
    }
    private static double addTo(double a, int n) {
    for (int i = 0; i < n; ++i) a = a + i;
    return a;
    }
    }

    2009 年 7 月 17 日 下午 4:06

  19. a

    “像 Java 这样的语言编译成机器语言,几乎不需要状态跟踪开销。”

    Java 编译成字节码,然后由 JVM 解释执行。虽然某些热点会被编译成机器语言以进行优化,但其余部分仍然是解释执行的。请纠正这一点。

    2009 年 7 月 17 日 下午 5:53

  20. Joao Pedrosa

    很好的解释。谢谢。

    文章和上面的一些信息性评论表明,Mozilla 意识到了用户的需求以及存在的挑战。

    我了解到,要启用和禁用 JIT,我不需要重新启动浏览器,这本身就很酷。我禁用了 JIT,因为几天前我遇到了安全警告。因此,启用和禁用它在这方面产生了巨大的影响。

    我也不知道“跟踪”部分意味着什么,这篇文章对此有所启发。

    我的 Javascript 代码通常有利于利用跟踪功能,因为我一直使用大量的“for 循环”并尝试节省方法调用。我还使用了一些“switch”语句,而不是“if”语句,无论是否有帮助。;-) 我有一点担心的是,在很少的情况下,会使用同一个变量或方法参数临时保存不同类型的数据,例如,当将第一个参数设为可选参数,但仍然使用命名参数,而不是“arguments”工具时。但这些情况很少见(就像我创建的方法中不到 1%)。

    我使用的是 Linux,通常等待 Ubuntu 发行版发布 Firefox 的新版本,因此,一旦安全补丁发布或出现其他情况,我就会再次启用 JIT。

    与往常一样,在尝试解决性能问题时,良好的算法可以节省不必要的计算,这一点非常重要。因此,请进行分析,找到瓶颈,尝试对其进行优化,并希望 Mozilla 的人员能够在他们的工作中为我们改善整体情况。:-)

    2009 年 7 月 17 日 下午 10:29

  21. Luke

    @a - 现代 JVM,即在基准测试中表现出色的 JVM,会将所有内容编译成机器代码(尽管有时是延迟编译),而不仅仅是热点。

    2009 年 7 月 17 日 下午 11:44

  22. Dan Hirsch

    因此,一种非常酷的方法是,您可以使用多态内联缓存来几乎达到非专业化 JIT 的性能,其中,方法的第一次跟踪使用对最通用类型的调用,并且随着类型信息的变得已知,对更特定函数的调用会被回补到代码中。有关更多详细信息,请参阅 http://research.sun.com/self/papers/pics.html

    2009 年 7 月 18 日 上午 1:42

  23. […] quieres leer de una forma mejor explicada la forma de trabajo de este motor, nada como ir al artículo original. Comparte esta […]

    2009 年 7 月 18 日 上午 5:17

  24. Boris

    David,我不确定您所说的“进行远程操作会使加速两倍的努力失效”是什么意思...

    Untraced Monkey,Robert,示例 3 中发生的事情相当有趣。每次点击按钮,第二个循环都会以不同的 mul 值运行。编译后的代码假定 mul 的特定值,并添加检查以确保实际值是它假定的值。所以,在第二次点击时,我们进入循环,开始运行编译后的代码,发现实际值与预期值不匹配,退出到解释器并使用新值重新跟踪循环。现在,我们有了这个循环的两个跟踪。

    在第三次点击时,我们进入调用,将 mul 的值与我们现有的两个跟踪进行比较,发现两个跟踪都不匹配,退出到解释器等等。

    请注意,这意味着每次点击都会稍微慢一些,因为每次点击都需要为“将 mul 的值与我们现有的跟踪进行比较”部分做更多工作。在我的测试中,这会使速度从 1-2 毫秒降至约 15 毫秒。

    如你可能已经注意到,我们在这里获得了越来越多的跟踪。正如文章中所述,当我们得到 32 个跟踪后,我们就会停止生成新的跟踪。因此,在那之后,在每次循环迭代中,我们调用编译后的代码,发现 mul 与我们 32 个跟踪中的任何一个都不匹配,退出到解释器。与前 32 次不同,我们不再能够创建新的跟踪,因此对于每次点击,我们都会执行上述调用编译代码然后回退操作 100000 次。这就是导致速度如此慢的原因(在我的机器上,大约比纯解释器慢 4 倍)。

    https://bugzilla.mozilla.org/show_bug.cgi?id=504829 是跟踪此问题的错误,对于那些想要了解更多关于如何调试此类问题以及 Brendan 和 David 在解决此类问题方面的想法的人来说,这是一个很好的资源。

    2009 年 7 月 18 日 08:17

  25. Kailas Patil

    解释很棒!非常有用。

    2009 年 7 月 19 日 06:07

  26. Jan!

    感谢你提供了这篇内容丰富的文章。我对内部机制了解不多,但你解释得非常清楚。

    2009 年 7 月 20 日 05:37

  27. Ken

    “JS 和其他动态脚本语言通常比 Java 或 C 运行速度慢的主要原因有两个。第一个原因是,在动态语言中,通常无法提前确定值的类型。……动态语言运行速度慢的第二个主要原因是,脚本语言通常使用解释器实现。”

    我不同意。任何程序运行缓慢的首要原因是程序员没有对其进行性能分析。脚本语言往往缺乏性能分析工具(或者根本没有)。在 Javascript 的情况下,我只见过一个性能分析工具,而且它很糟糕。

    如果你编写了一个冒泡排序算法,即使使用 C 也无法帮助你。我不认识任何在 Javascript 中编写冒泡排序算法的人,但我也没有在任何我读过的 JS 文档中看到任何时间/空间使用说明。我们可能正在编写与冒泡排序算法一样愚蠢的代码,却浑然不知。

    (我内心中的愤世嫉俗者说:当然,如果你让程序员独自一人,他们会把一些流行的 1980 年代 Smalltalk 和 Lisp 技术放到浏览器中。这很容易。为程序员编写一个可用的性能分析工具是一项艰巨的任务,因为它涉及到实际的用户界面。)

    尽管如此,我并不抱怨:有 Tracemonkey 总比没有 Tracemonkey 好!我只是希望有人能编写一个像样的性能分析工具。:'-(

    2009 年 7 月 20 日 12:07

  28. […] 在 hacks.mozilla.org 上写了一篇关于 TraceMonkey 的文章。这篇文章的目标读者是 Web 开发人员和任何想要了解 TraceMonkey 如何工作的人 […]

    2009 年 7 月 20 日 13:44

  29. Dave Mandelin

    Ron:感谢你提供了有关 Java 的信息。我以为 Java 的时间与 C 在类似的微基准测试中相似,所以我的结果让我很惊讶。你澄清了这一点。我使用的是客户端 VM。我的测试代码与你的类似,只是我运行 |runXTest| 仅运行一次,而不是 100 次。我尝试使用服务器 VM,双精度版本的时间从 75 毫秒降至 20 毫秒,符合你的预测。我认为,随着运行次数的增加,你会达到更高的优化级别,从而提高整体吞吐量。此外,服务器 VM 的启动时间似乎更长,这在 Web 上可能会或可能不会重要,具体取决于 VM 的管理方式。很难说哪种设置更适合基准测试,因为它完全取决于你真正关心的工作负载。我们今年夏天正在进行一些关于 Web 代码的研究,以期了解更多相关信息。

    a:是的,当我提到 Java 编译为本地代码时,我的说法比较宽泛。我认为,在现代高性能 JVM 中,大多数性能关键方法的大多数运行都是作为本地代码运行的。这种细节与我想要讨论的主要内容无关,因此我简化了说明。

    Dan Hirsh:感谢你提供这篇论文的链接。我知道 PIC 技术,并且我希望(或者至少希望)我们将在 TraceMonkey 中开始使用它来处理某些事情,但我以前没有遇到过关于 PIC 的良好详细技术介绍。

    2009 年 7 月 20 日 14:24

  30. Alex Merk

    在 Google Chrome(使用最新的 WebKit)中,只有第一个示例工作正常。其他两个示例都出现了错误。

    2009 年 7 月 20 日 17:45

  31. Lindsey

    好文章。开发人员了解这方面的更多信息至关重要 - 我们都对 JS 中的这些改进感到非常兴奋,但我们还不确定如何充分利用它。

    我想提到的另一件事是,当你对 JS 进行性能分析时(例如通过 firebug),跟踪实际上是被禁用的。所以,使用它作为获取时间的方法时要小心。

    有关详细信息,请参见 http://code.google.com/p/fbug/issues/detail?id=1833

    2009 年 7 月 20 日 17:46

  32. […] 原文地址:an overview of TraceMonkey 系列地址:颠覆网络35天 […]

    2009 年 7 月 21 日 02:24

  33. […] David Mandelin 大方地详细介绍了跟踪以及 TraceMonkey 的概述。[…]

    2009 年 7 月 21 日 04:08

  34. […] David Mandelin 大方地详细介绍了跟踪以及 TraceMonkey 的概述。[…]

    2009 年 7 月 21 日 07:10

  35. […] Mandelin 是 Mozilla 的 JavaScript 团队成员,最近发布了一篇关于跟踪树和 TraceMonkey JS 引擎的精彩概述文档。对于那些在 Web 应用程序和 Web 中大量使用 JavaScript 的人来说,这是一篇必读的文章 […]

    2009 年 7 月 21 日 10:47

  36. […] Mandelin 大方地详细介绍了跟踪以及 TraceMonkey 的概述 […]

    2009 年 7 月 21 日 13:41

  37. […] David Mandelin 大方地详细介绍了跟踪以及 TraceMonkey 的概述。[…]

    2009 年 7 月 21 日 15:22

  38. Edward Rudd

    我认为,第三个示例在多次运行后速度变慢的一个可能原因可能是每次运行时都会创建该函数,并且你可能遇到了垃圾收集问题。我单独调整了该函数并测试了几个变体。
    其中一个使用 var mul= function(i) {}; 并删除了 eval(相同的效果)。它仍然增加到大约 250-300 毫秒。

    另一个将函数/eval 移动到方法顶部并让它接受两个参数(避免闭包)。相同的结果。

    将代码更改为 prod = prod * i。这修复了这个问题,使其每次都运行 1-2 毫秒。

    现在这很奇怪.. 我将 mul 函数声明移到了函数外部,因此它只创建一次,并接受两个参数(prod 和 i)。并从示例 3 函数中调用 mul(prod,i)。现在它每次都持续 250 毫秒。

    2009 年 7 月 22 日 08:23

  39. Edward Rudd

    以下是对示例 3 的一些有趣的调查。

    我单独调整了该函数并测试了几个变体。

    其中一个使用 var mul= function(i) {}; 并删除了 eval(相同的效果)。它仍然增加到大约 250-300 毫秒。

    另一个将函数/eval 移动到方法顶部并让它接受两个参数(避免闭包)。相同的结果。

    将代码更改为 prod = prod * i。这修复了这个问题,使其每次都运行 1-2 毫秒。

    现在这很奇怪.. 我将 mul 函数声明移到了函数外部,因此它只创建一次,并接受两个参数(prod 和 i)。并从示例 3 函数中调用 mul(prod,i)。现在它每次都持续 250 毫秒。

    2009 年 7 月 22 日 08:24

  40. […] 链接:hacks.mozilla.org 上的 TraceMonkey 概述 […]

    2009 年 7 月 22 日 21:54

  41. Boris

    Edward,你能发布始终很慢的代码吗?

    2009 年 7 月 23 日 11:04

  42. […] https://hacks.mozilla.ac.cn/2009/07/tracemonkey-overview/ :对 TraceMonkey(Firefox 3.5 背后的 JS 引擎)工作原理的解释 […]

    2009 年 7 月 28 日 09:09

  43. […] TraceMonkey 的概述,FireFox v3.5 的 JavaScript 引擎。[…]

    2009 年 8 月 1 日 20:42

  44. […] TraceMonkey JavaScript 引擎一直在不断改进 […]

    2009 年 8 月 7 日 19:40

  45. […] TraceMonkey JavaScript 引擎正在改进。[…]

    2009 年 8 月 9 日 00:32

  46. […] 3.6 的功能包括更快的 JavaScript(Firefox 使用其 TraceMonkey 引擎执行的 Web 编程语言);更快的页面渲染速度;一些针对 CSS(层叠样式表)技术的新功能,用于 […]

    2009 年 8 月 9 日 01:01

  47. […] 3.6 的功能包括更快的 JavaScript(Firefox 使用其 TraceMonkey 引擎执行的 Web 编程语言);更快的页面渲染速度;一些针对 CSS(层叠样式表)技术的新功能,用于 […]

    2009 年 8 月 9 日 01:10

  48. […] 3.6 的功能包括更快的 JavaScript(Firefox 使用其 TraceMonkey 引擎执行的 Web 编程语言);更快的页面渲染速度;一些针对 CSS(层叠样式表)技术的新功能,用于 […]

    2009 年 8 月 9 日 03:20

  49. […] 3.6 的功能包括更快的 JavaScript(Firefox 使用其 TraceMonkey 引擎执行的 Web 编程语言);更快的页面渲染速度;一些针对 CSS(层叠样式表)技术的新功能,用于 […]

    2009 年 8 月 9 日 03:50

  50. […] Firefox 3.5 在此测试中甚至比 Firefox 3.0 还要慢。这可能是因为我没有针对 Tracemonkey 进行优化,但 Firefox 的性能并不如预期。无论如何,测试结果与 […]

    2009 年 8 月 10 日 00:02

  51. Juan Lim

    太棒了!更强大的功能。继续努力!

    2009 年 8 月 10 日 10:06

  52. […] TraceMonkey 引擎的速度提升,以及对 Gecko 的一些调整 […]

    2009 年 8 月 12 日 16:40

  53. […] 原文地址:an overview of TraceMonkey 系列地址:颠覆网络35天 […]

    2009 年 8 月 19 日 22:30

  54. […] 函数,以推动 Mozilla 产品和技术的关键方面的改进,包括 JavaScript 性能、Canvas 功能、Bespin 的应用等,* 接触并增加 […]

    2009 年 9 月 10 日 10:54

  55. anon_anon

    你可能想看看 vtd-xml,它是 XML 处理领域的最新技术,比 DOM 消耗的内存少得多。

    vtd-xml

    2009 年 11 月 27 日 12:17

  56. przemelek

    您好,文章不错,但我有问题,为什么在 FF 3.5.5 中,我的笔记本电脑只需要大约 43 毫秒(平均值),但在 FF 3.6 beta 5 中却需要 850 毫秒?
    3.5.5 的配置看起来与 3.6 相同,但 3.6 beta 5 在此测试中慢了 19 倍。

    2009 年 12 月 22 日 18:25

    1. Dave Mandelin

      是否可能你的 3.6 配置中关闭了跟踪功能?我刚刚在 3.6 nightly 中试了一下,我发现如果我在 about:config 中将 javascript.options.jit.content 设置为 false,则第一个测试会很慢,但如果设置为 true(新配置文件的默认值),则运行时间约为 45 毫秒,与 3.5 相同。

      2010 年 1 月 6 日 18:51

  57. […] Javascript 的 JIT 后端,原来 firefox 直接使用 javascript 解释器,效率比较低。nanojit 可以将频繁执行的 javascript 代码直接翻译为机器码执行,效率更高,性能更好。详细的介绍可以参看这篇文章:an overview of TraceMonkey,  (我是中国人,我要看中文 ) […]

    2009 年 12 月 23 日 19:44

  58. […] 还有 hacks 文章,其中详细介绍了跟踪的背景 […]

    2010 年 2 月 26 日 15:40

  59. CAFxX

    我想知道 LLVM 是否曾经被考虑过作为 JIT 的替代方案,显然是在它之上实现跟踪。

    2010年2月26日 下午11:24

  60. [...] 火狐的引擎在根本上与其他所有引擎不同:其他人使用的是所谓的“基于方法的 JIT”。也就是说,他们将所有传入的 JS 代码编译成机器码,然后执行它。火狐使用的是“跟踪 JIT”。我们解释所有传入的 JS 代码,并在解释时进行记录。当我们检测到一个热点时,我们会将它转换成机器码,然后执行内部部分。(有关跟踪的更多背景信息,请参阅去年关于黑客的这篇文章。)[...]

    2010年3月1日 上午11:42

  61. Dave Mandelin

    @CAFxX

    LLVM 已经被讨论过了,但显然我们还没有准备好尝试它。我认为最大的问题是代码大小。它还比我们现在需要的优化做得更多,并且可能需要更长的时间来编译东西(虽然我真的不知道,我认为你可以关闭优化)。看来目前我们可以用更简单的系统来完成我们需要的任务,所以这就是我们一直在做的事情。不过,我们应该把 LLVM 牢记在心。

    2010年3月1日 下午2:23

  62. Jeff Walden

    CAFxX,也值得考虑一下 unladen-swallow 项目的结果。对于我们一直在做的事情,LLVM 可能不一定是最好的选择。

    2010年3月1日 下午2:56

  63. [...] 代码。许多 JavaScript 程序在 TraceMonkey 中的运行速度比 Firefox 3 快 3-4 倍。(参见我们之前的文章,了解技术 [...]

    2010年3月10日 上午9:52

  64. [...] 转换成机器码并在内部执行。(有关 Tracing 的更多信息,请参阅去年关于黑客的帖子 [...]

    2010年8月12日 下午5:32

  65. Delarke

    非常有趣,搜索得很好——我从这篇博文中学习到了一些很好的点。

    2011年1月14日 上午10:20

  66. MIZ-Networx

    伟大的文章,JIT 带来惊人的加速。

    2011年3月2日 下午1:19

  67. [...] 代码。许多 JavaScript 程序在 TraceMonkey 中的运行速度比 Firefox 3 快 3-4 倍。(参见我们之前的文章,了解技术 [...]

    2012年3月3日 上午9:47

本文的评论已关闭。