【问题标题】:Firefox JavaScript arithmetics performance oddityFirefox JavaScript 算术性能异常
【发布时间】:2023-04-03 11:19:01
【问题描述】:

请在 Firefox 上运行此测试。

http://jsperf.com/static-arithmetic

你会如何解释结果?

这个

b = a + 5*5;
b = a + 6/2;
b = a + 7+1;

执行速度比

快得多
b = a + 25;
b = a + 3;
b = a + 8;

为什么?

【问题讨论】:

  • 在帖子中非常具体(包括标签和标题!)。在 Windows 中的 Firefox 7.0.1 中,我的数字一致——第二次测试运行速度慢了 30-60%。查看基准(现在已经有很多人进行了测试;-)表明这种现象似乎始于 FF 4.x 并且不会影响 Chrome ——也就是说,它不是 JavaScript 的内在属性。请相应地更新问题。
  • 我颠倒了测试订单只是为了确保钻机不会导致这种情况;不是。
  • 在 Windows XP 上的 Firefox 5 中,两者的速度差不多(差别不大)。在 IE 8 中,第一个速度慢了 20%(也可能微不足道)。正如 PST 所说,它与 javascript per se 无关,与特定平台上的实现有很大关系。

标签: javascript performance firefox arithmetic-expressions


【解决方案1】:

Firefox 版本 4-8 有两种不同的 JIT:Tracemonkey (tracejit) 和 JaegerMonkey (methodjit)。 TraceMonkey 在简单的数字代码上要好得多; JaegerMonkey 在处理各种分支代码时要好得多。

有一个启发式方法用于决定使用哪个 JIT。它着眼于一大堆因素,其中大部分在这里无关紧要,但对于这个测试用例来说重要的是循环体中的算术操作越多,使用 TraceMonkey 的可能性就越大。

您可以通过更改 javascript.options.tracejit.contentjavascript.options.methodjit.content 的值来测试这一点,以强制代码在一个或另一个 JIT 下运行,然后查看它如何影响性能。

看起来恒定折叠并没有节省时间,因为 Spidermonkey 不能将 a + 7 + 1 = (a + 7) + 1 恒定折叠到 a + 8 因为它不知道 a 是什么(例如,"" + 7 + 1 == "71""" + 8 == "8")。如果你把它写成a + (7 + 1),那么你会突然在这段代码上运行另一个 JIT。

所有这些都证明了从微基准推断到实际代码的危险。 ;)

哦,Firefox 9 只有一个 JIT(JaegerMonkey 基于 Brian Hackett 的类型推断工作进行了优化,使其在此类算术代码上也能快速运行)。

【讨论】:

    【解决方案2】:

    在 Firefox 中,它看起来与浮点数学与整数数学有关,其中浮点运算要快得多。当我添加一些浮点数学时,您可以看到区别:http://jsperf.com/static-arithmetic/14

    这要快得多:

    b = a + 26.01;
    b = a + 3.1;
    b = a + 8.2;
    

    比这个:

    b = a + 25;
    b = a + 3;
    b = a + 8;
    

    我只能猜测,Firefox 有一些浮点优化不适用于整数数学,或者代码在涉及浮点数时只是采用了不同的路径。

    因此,将此信息推断为您的原始答案,+ 5*5 必须使用更快的浮动路径,而 + 25 不是。有关详细信息,请参阅referenced jsPerf

    一旦你让一切都浮动,+ (5.1 * 5.1) 选项会比我们预期的+ 26.01 选项慢。

    【讨论】:

      【解决方案3】:

      首先,你的测试有一点瑕疵。

      您应该比较以下内容:

      • b = a + 8 - 2;b = a + 6

      • b = a + 8 + 2;b = a + 10

      • b = a + 8 / 2;b = a + 4

      • b = a + 8 * 2;b = a + 16

      您会注意到一些有趣的事情:只有第二对术语中有+- 的问题比较慢(除法和乘法都可以)。加法/减法和乘法/除法的实现之间必须有明显的区别。确实有:

      让我们看看加法和乘法(jsparse.cpp):

          JSParseNode *
          Parser::addExpr()
          {
              JSParseNode *pn = mulExpr();
              while (pn &&
                     (tokenStream.matchToken(TOK_PLUS) ||
                      tokenStream.matchToken(TOK_MINUS))) {
                  TokenKind tt = tokenStream.currentToken().type;
                  JSOp op = (tt == TOK_PLUS) ? JSOP_ADD : JSOP_SUB;
                  pn = JSParseNode::newBinaryOrAppend(tt, op, pn, mulExpr(), tc);
              }
              return pn;
          }
      
          JSParseNode *
          Parser::mulExpr()
          {
              JSParseNode *pn = unaryExpr();
              while (pn && (tokenStream.matchToken(TOK_STAR) || tokenStream.matchToken(TOK_DIVOP))) {
                  TokenKind tt = tokenStream.currentToken().type;
                  JSOp op = tokenStream.currentToken().t_op;
                  pn = JSParseNode::newBinaryOrAppend(tt, op, pn, unaryExpr(), tc);
              }
              return pn;
          }
      

      但是,我们可以看出,这里并没有太大的不同。两者都以类似的方式实现,并且都调用newBinaryOrAppend().. 那么这个函数到底是什么?

      (剧透:它的同名可能会背叛为什么加法/减法更昂贵。再次查看 jsparse.cpp

      JSParseNode *
      JSParseNode::newBinaryOrAppend(TokenKind tt, JSOp op, JSParseNode *left, JSParseNode *right,
                                     JSTreeContext *tc)
      {
          JSParseNode *pn, *pn1, *pn2;
      
          if (!left || !right)
              return NULL;
      
          /*
           * Flatten a left-associative (left-heavy) tree of a given operator into
           * a list, to reduce js_FoldConstants and js_EmitTree recursion.
           */
          if (PN_TYPE(left) == tt &&
              PN_OP(left) == op &&
              (js_CodeSpec[op].format & JOF_LEFTASSOC)) {
              if (left->pn_arity != PN_LIST) {
                  pn1 = left->pn_left, pn2 = left->pn_right;
                  left->pn_arity = PN_LIST;
                  left->pn_parens = false;
                  left->initList(pn1);
                  left->append(pn2);
                  if (tt == TOK_PLUS) {
                      if (pn1->pn_type == TOK_STRING)
                          left->pn_xflags |= PNX_STRCAT;
                      else if (pn1->pn_type != TOK_NUMBER)
                          left->pn_xflags |= PNX_CANTFOLD;
                      if (pn2->pn_type == TOK_STRING)
                          left->pn_xflags |= PNX_STRCAT;
                      else if (pn2->pn_type != TOK_NUMBER)
                          left->pn_xflags |= PNX_CANTFOLD;
                  }
              }
              left->append(right);
              left->pn_pos.end = right->pn_pos.end;
              if (tt == TOK_PLUS) {
                  if (right->pn_type == TOK_STRING)
                      left->pn_xflags |= PNX_STRCAT;
                  else if (right->pn_type != TOK_NUMBER)
                      left->pn_xflags |= PNX_CANTFOLD;
              }
              return left;
          }
      
          /*
           * Fold constant addition immediately, to conserve node space and, what's
           * more, so js_FoldConstants never sees mixed addition and concatenation
           * operations with more than one leading non-string operand in a PN_LIST
           * generated for expressions such as 1 + 2 + "pt" (which should evaluate
           * to "3pt", not "12pt").
           */
          if (tt == TOK_PLUS &&
              left->pn_type == TOK_NUMBER &&
              right->pn_type == TOK_NUMBER) {
              left->pn_dval += right->pn_dval;
              left->pn_pos.end = right->pn_pos.end;
              RecycleTree(right, tc);
              return left;
          }
      
          pn = NewOrRecycledNode(tc);
          if (!pn)
              return NULL;
          pn->init(tt, op, PN_BINARY);
          pn->pn_pos.begin = left->pn_pos.begin;
          pn->pn_pos.end = right->pn_pos.end;
          pn->pn_left = left;
          pn->pn_right = right;
          return (BinaryNode *)pn;
      }
      

      鉴于上述情况,尤其是常量折叠:

      if (tt == TOK_PLUS &&
          left->pn_type == TOK_NUMBER &&
          right->pn_type == TOK_NUMBER) {
          left->pn_dval += right->pn_dval;
          left->pn_pos.end = right->pn_pos.end;
          RecycleTree(right, tc);
          return left;
      }
      

      并考虑到在制定问题时

      • b = Number(a) + 7 + 2;b = Number(a) + 9;

      ...问题完全消失了(尽管由于我们正在调用静态方法,它显然要慢得多),我很想相信任何一个常量折叠都被破坏了(这似乎不太可能,因为括号折叠出现了工作正常),Spidermonkey 没有将数字文字(或数字表达式,即b = a + ( 7 + 2 ))分类为TOK_NUMBER(至少在第一个解析级别),这也不太可能,或者我们正在递归地下降到某个地方太深了。

      我没有使用过 Spidermonkey 代码库,但我的蜘蛛侠感觉告诉我我们迷路了,我感觉它在 RecycleTree()

      【讨论】:

      • 这是对不同问题的答案,还是 OP 没有提到的一些历史?
      • 它回答了 OP 的问题。引用的 C++ 代码可在 Spidermonkey 源代码中找到,Firefox 将其用作其 Javascript 引擎。
      • @David 您正在查看 Spidermonkey 解析器和字节码编译器。上面代码的输出用作 JIT 编译器的输入,它自己进行优化。您正在查看的代码尤其不是在需要添加时运行的代码;仅在解析 JavaScript 输入时才开始。
      【解决方案4】:

      在 Windows XP 上测试 Firefox 3.6.23 测试操作/秒 赋值算术

      b = a + 5*5;
      b = a + 6/2;
      b = a + 7+1;
      
      67,346,939 ±0.83%11% slower assign plain
      
      
      b = a + 25;
      b = a + 3;
      b = a + 8;
      
      75,530,913 ±0.51%fastest
      

      【讨论】:

        【解决方案5】:

        在 Chrome 中不是这样。

        对我来说:

        b = a + 5*5;
        b = a + 6/2;
        b = a + 7+1;
        

        结果:267,527,019,±0.10%,慢 7%

        b = a + 25;
        b = a + 3;
        b = a + 8;
        

        结果:288,678,771,±0.06%,最快

        所以,不是真的...不知道为什么它会在 Firefox 上这样做。

        (在 Windows Server 2008 R2 / 7 x64 上的 Chrome 14.0.835.202 x86 中测试)

        【讨论】:

        • 这就是我问火狐的原因。它是特定于蜘蛛猴的.. 错误?查看测试下方的图表。
        • 没有。 “测试”没有显示平台,这可能是一个更重要的因素。
        猜你喜欢
        • 2021-03-29
        • 2014-12-16
        • 1970-01-01
        • 1970-01-01
        • 2019-04-08
        • 1970-01-01
        • 2018-04-22
        • 1970-01-01
        • 2012-09-12
        相关资源
        最近更新 更多