Firefox JavaScript算术表现奇怪

时间:2011-10-12 21:55:53

标签: javascript performance firefox arithmetic-expressions

请在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;

为什么?

5 个答案:

答案 0 :(得分: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不是。+ (5.1 * 5.1)。有关详细信息,请参阅referenced jsPerf

一旦你把所有东西都浮起来,+ 26.01选项比{{1}}选项慢,就像我们期望的那样。

答案 1 :(得分:3)

首先,您的测试存在轻微缺陷。

您应该比较以下内容:

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

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

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

  • b = a + 8 * 2; vs 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; vs b = Number(a) + 9;

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

我没有使用Spidermonkey代码库,但我的Spidey意识告诉我,我们在某个地方迷路了,我感觉它在RecycleTree()

答案 2 :(得分: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,因为它不知道{{1}是(例如,a"" + 7 + 1 == "71")。如果你把它写成"" + 8 == "8",那么突然你会在这段代码上运行另一个JIT。

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

哦,Firefox 9只有一个JIT(JaegerMonkey基于Brian Hackett的类型推理工作进行了优化,使得它在这类算术代码上也很快)。

答案 3 :(得分:0)

在Windows XP上以Firefox 3.6.23测试Ops /秒 分配算术

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

答案 4 :(得分:-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进行测试)