Почему 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
Ответы
Ответ 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 должен перейти к цепочке объектов, чтобы найти переменную или функцию.
Изменить: Возможно, это не так.