Странность производительности арифметики JavaScript в Firefox - PullRequest
12 голосов
/ 13 октября 2011

Пожалуйста, запустите этот тест на 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 ]

3 голосов
/ 13 октября 2011

Прежде всего, ваш тест слегка испорчен.

Вы должны сравнить следующее:

  • 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, но мое чувство Spidey говорит мне, что мы где-то теряемся, и у меня такое ощущение, что оно в RecycleTree().

3 голосов
/ 13 октября 2011

В 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, как мы и ожидали.

1 голос
/ 13 октября 2011

Firefox версии 4-8 имеют два разных JIT: Tracemonkey (tracejit) и JaegerMonkey (methodjit).TraceMonkey намного лучше работает с простым числовым кодом;JaegerMonkey гораздо лучше разбирается в ветвящемся коде различных типов.

Существует эвристика, которая используется для определения, какой JIT использовать.В нем рассматривается множество факторов, большинство из которых здесь неактуальны, но для этого теста важен тот факт, что чем больше арифметических операций в теле цикла, тем более вероятно использование TraceMonkey.

Выможно проверить это, изменив значения javascript.options.tracejit.content и javascript.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 с оптимизацией, основанной на работе вывода типа Брайана Хакетта, которая делает его быстрым и для арифметического кода такого рода).

0 голосов
/ 13 октября 2011

Тестирование в Firefox 3.6.23 в Windows XP Test Ops / sec назначить арифметику

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
0 голосов
/ 13 октября 2011

Не верно в 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.

(Тестирование в Chrome 14.0.835.202 x86 на Windows Server 2008 R2 / 7 x64)

...