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

Почему Math.round в javascript медленнее, чем настраиваемая функция?

Я возился с созданием специальной функции округления, которая могла бы округляться до любого интервала, который я хотел. например (если я работал со степенями, округлял бы до ближайших 15 градусов). В любом случае я решил посмотреть, насколько быстро он сравнивается с Math.round и выясняется, что он медленнее. Я использую firebug на FF8

function R1(a,b){var c=a%b;return a-c+(c/b+1.5>>1)*b}

function R2(a){return Math.round(a)}

var i,e=1e5;
console.time('1');
i=e;
while(i--){
  R1(3.5,1);
}
console.timeEnd('1');
console.time('2');
i=e;
while(i--){
  R2(3.5);
}
console.timeEnd('2');

и мои результаты были

1: 464ms
2: 611ms

Я запускал их несколько раз по-разному, но всегда R1 выходил быстрее. Возможно, это всего лишь вещь FF, но если да, то что ее вызывает.

EDIT: Я взял каждый вызов функции, чтобы узнать, что произойдет

var i,e=1e5,c;
console.time('1');
i=e;
while(i--){
  c=3.5%1;
  3.5-c+(c/1+1.5>>1)*1;
}
console.timeEnd('1');
console.time('2');
i=e;
while(i--){
  Math.round(3.5);
}
console.timeEnd('2');

и время, когда я получаю

1: 654ms
2: 349ms
4b9b3361

Ответ 1

Короткий ответ заключается в том, что в Firefox 8 (но не в 9) Math.round заканчивается вызовом функции С++, которая медленна в JIT. Долгий ответ заключается в том, что он сложный, и он отличается от разных версий Firefox. Кроме того, поскольку JITs задействованы, это будет отличаться для разных процессоров и ОС.

Немного фона: в соответствии с ECMA-262, раундами Math.round до ближайшего целого числа, за исключением того, что для 0.5, он округляется до + Inf, а для [-0.5, -0.0] округляется до -0.0 (IEEE -754 отрицательный ноль). Чтобы получить это право, Math.round должен делать больше, чем R1. Для выполнения диапазона с округлением до -0 (который V8 делает), или скопируйте знак с входа (что делает SpiderMonkey), потребуется либо выполнить сравнение с плавающей запятой для диапазона, который округляется до -0 (что делает V8).

Теперь, для Firefox 8, обе петли собираются с помощью tracejit. Для цикла с R1 R1 встраивается и компилируется в чистый собственный код. R2 встроен и скомпилирован для вызова функции С++, называемой js_math_round_impl (в js/src/jsmath.cpp).

  • Вызов какой-либо функции стоит дополнительно, поскольку параметры необходимо настроить, сделать вызов, переместить регистры и т.д.

  • Вызов Math.round или тому подобное стоит дополнительно, потому что код должен проверить, что Math.round по-прежнему является значением по умолчанию Math.round(т.е. не проверяет monkeypatching).

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

  • И, как упоминалось ранее, Math.round должен выполнять больше работы, чем R1.

Я попробовал несколько разных тестов в JS и C, чтобы попытаться выяснить, является ли вызов более важным или проверка -0. Результаты варьировались, но похоже, что вызов был, как правило, большей частью замедления (70-90% от него).

В Firefox 9 с JM + TI R1 и R2 примерно одинаково быстры. В этом случае R1 снова становится встроенным (я думаю) и скомпилирован в чистый собственный код. Для R2 Math.round реализуется куском jitcode, который обрабатывает положительные числа напрямую, но вызывает функцию С++ для отрицательных чисел (и NaN и т.д.). Итак, для приведенного примера оба запускаются в jitcode, а R2 - немного быстрее.

В общем, с такими функциями, как Math.round(то, что традиционно было вызовом функции С++, но достаточно просто, что по крайней мере некоторые случаи могут выполняться непосредственно в jitcode), производительность будет зависеть от многих на сколько оптимизации jitcode разработчики движка сделали для этой конкретной функции.

Ответ 2

Сравнение фактически неверно. R2() - это функция, вызывающая Math.round(). R1() выполняет округление напрямую.

Итак, R2 включает дополнительный вызов функции - это медленная операция.

Попробуйте сравнить реализацию округления с теми же условиями:

function R1(a,b){var c=a%b;return a-c+(c/b+1.5>>1)*b}
R2 = Math.round;

Кредиты идут к Кевину Балларду, который предложил переместить Math.round() из R2().

Смотрите: http://jsperf.com/comparing-custom-and-bult-in-math-round.

Обновление:

Результаты для Firefox очень разные, чем для Chrome.

Примечание. Я неопытен в этой области, поэтому я предполагаю, что здесь. Если кто-то испытает, он сможет взять на себя эти цифры, это было бы потрясающе.

Похоже, что Firefox сильно оптимизирует, когда входное значение не меняется. Он может оптимизировать R1(3.5) таким образом, но оптимизировать Math.round, вероятно, труднее оптимизировать из-за динамического характера JavaScript. Math.round реализация может измениться в любой момент во время выполнения кода. R1() использует только арифметические и побитовые операции. Производительность функций с использованием встроенных Math.round (R2() и R3()) находится на одном уровне с другими браузерами (кроме IE 9: o)).

У кого-то была отличная идея и была создана вторая ревизия тестового сценария:

http://jsperf.com/comparing-custom-and-bult-in-math-round/2.

Эта ревизия также тестирует выполнение функций, в которых значение, переданное им, изменяется.

Любая идея, почему Built-in Math.round настолько результативно сравнивается даже с пользовательским округлением со статическим входом?

Ответ 3

Я не помню точный источник, но это было видео-беседа Google, обсуждающая это. Поле, которое является частью объекта (например, this.field), медленнее, чем прямая ссылка, поскольку Javascript должен перейти к цепочке объектов, чтобы найти переменную или функцию.

Изменить: Возможно, это не так.