火狐浏览器JavaScript算术性能的奇怪问题

12
请在火狐浏览器上运行此测试。
请访问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;

为什么?


1
在帖子中要非常具体(包括标签和标题!)。在 Windows 的 Firefox 7.0.1 中,我的数字是一致的——第二个测试运行速度较慢,大约慢了30-60%。查看基准测试(现在许多人都运行了测试 ;-)),表明这种现象似乎始于 FF 4.x,并且不影响 Chrome——也就是说,这根本不是 JavaScript 的固有属性。请相应地更新问题。 - user166390
我调整了测试顺序,只是为了确保硬件不会引起问题;事实证明并非如此。 - Hemlock
在Windows XP上的Firefox 5中,这两者的速度大致相同(差异微不足道)。在IE 8中,第一个要慢20%(可能也微不足道)。正如PST所说,这与JavaScript本身无关,而与特定平台上的实现有很大关系。 - RobG
5个回答

3
在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则不是。请参见引用的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;
}

鉴于以上内容,尤其是常量折叠(constant folding):
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没有提到的? - blankabout
它回答了OP的问题。引用的C++代码可以在Spidermonkey源代码中找到,这是Firefox作为其Javascript引擎使用的内容。 - David Titarenco
@David,你正在查看Spidermonkey解析器和字节码编译器。上面代码的输出作为JIT编译器的输入,该编译器进行自己的优化。你要查看的代码特别是_不是_在执行加法时运行的代码;只有在最初解析JavaScript输入时才会运行。 - Boris Zbarsky

1

火狐浏览器版本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的类型推断工作进行优化,使其在这种类型的算术代码上也很快。


0

在 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

-1

在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进行测试)


这就是为什么我问关于Firefox的原因。这是SpiderMonkey特定的错误吗?请看测试下面的图表。 - Frizi
不。这些“测试”并没有显示平台,而平台很可能是一个更重要的因素。 - RobG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接