Подтвердить что ты не робот

Неопределенность производительности арифметики JavaScript JavaScript JavaScript

Запустите этот тест на 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;

Почему?

4b9b3361

Ответ 1

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

Ответ 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 нет. Подробнее см. ссылка jsPerf.

Как только вы сделаете все плавающим, параметр + (5.1 * 5.1) будет медленнее, чем опция + 26.01, как и следовало ожидать.

Ответ 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().. так что конкретно IS в этой функции?

(Спойлер: его тезка может предать, почему добавление/вычитание является более дорогостоящим.Снова, глядя на 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().

Ответ 4

Тестирование в 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

Ответ 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.

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