Previous Entry Share Next Entry
Быстродействие округления в Javascript
rawgift
Сегодня на работе задался интересным вопросом: какая из математических функций округления работает быстрей и почему — Math.round(), Math.floor() или Math.ceil(). Докопаться, почему, так и не удалось — я скачал гугловский v8, написанный на C++, но реализацию математических функций найти не смог. Слишком много кода, а времени на работе мало. Пришлось делать все выводы на основе собственных опытов.

Округление числе в Javascript

Для начала обнаруживаю, что round() и floor() работают примерно с одинаковой скоростью, а вот ceil() сильно отстаёт по скорости — разница не менее 2 раз не в пользу ceil(). Тест проводится достаточно примитивно: вызываем каждую функцию миллион раз, округляя псевдослучайное число, сохраняем результат, и так 100 раз. А потом выводим среднее арифметическое ста результатов.

Будьте аккуратны, браузер (или вкладка в случае с хромом) подвиснет секунд на 20-30!

Ради интереса добавляем вызов Number.toFixed(0), что эквивалентно вызову Math.round() — функция округляет аргумент до ближайшего целого числа. И видим, что Number.toFixed() медленнее Math.round() примерно в 10 раз, и использовать её для округления нельзя!

Ну и чтобы повеселиться от души: попробуем для «округления» использовать parseInt(). Делать этого категорически не рекомендую! Хе: на удивление, округление с помощью parseInt() работает всего в 1,5 раза медленнее, чем round(). Правда пользоваться им всё равно не стоит. А если приспичит, то обязательно указывайте второй необязательный аргумент — основание системы счисления.

Получается, что джавскриптовые библиотечные функции для округления вниз или до ближайшего целого работают хорошо, а безусловно вверх — нет. Вова Цванг рассказал, что можно использовать двойное побитовое отрицание (~~) для округления числа в большую сторону, прибавляя к получившемуся числу единицу. Быстродействие побитовой операции с прибавлением единицы получилась даже на 10% выше, чем у floor(). Однако у неё есть проблемы с отрицательными и целыми числами — об этом дальше. Андрейка предложил использовать операцию минус-тильда (-~), чтобы добиться того же результата. Лично мне так тоже больше нравится.

Интересно, что оба варианта округления с бинарным отрицанием работают даже немного быстрей (разница около 5 %), чем самый быстрый Math.floor(), поэтому вариант без прибавления единицы можно попробоавть использовать в качестве немного более быстрой альтернативы округлению вниз.

С очередным дописанным в основной цикл куском хром начал выплёвывать предупреждения о том, что вкладка наверняка зависла. Я не стал усложнять код, переписывая его для отложенного выполнения, но в следующих примерах лишнее удалю.

Отрицательные числа и нуль
Для начала убедимся, что библиотечные функции округления работают правильно с отрицательными и нулевыми аргументами. Сравниваем их поведение с нашими «идентичными функциями» и обнаруживаем, без удивления, что идентично библиотечным функциям округления на отрицательных числах работает только toFixed(), бесполезная из-за низкой производительности. При этом если убрать прибавление единицы к оператору ~~, то он заработает как надо.

Чтобы вызвать метод Number для числового литерала, его (в данном случае нуль) надо обрамить скобками. В противном случае (я лишь предполагаю!) интерпретатор Javascript пытается найти объект с именем «0», а сделать этого не может, т.к. идентификаторы не могут начинаться с цифры.

Целые числа
Совершенно очевидно, что округлённое целое число равно самому себе. Проверяем и обнаруживаем, что побитовые «тильда-тильда» и «тильда плюс 1» операции прибавляют ненужную единицу. Если избавится от этого ненужного эффекта, можно написать более быстрые функции округления вниз и вверх Math.floorFast() и Math.ceilFast() на основе побитовых операций. Они могут пригодиться в задачах, где производительность критична: например, в обработке графики.


Попытка написать быстрое округление
Казалось бы очевидное решение: обернуть нехитрую логику, обнаруженную выше, в отдельные функции и использовать их не сработало. Операция взятия остатка по-прежнему дорога́, то же самое относится и к немногочисленным, но присутствующим условиям. В итоге моё «быстрое» округление вниз работает почти в 2 раза дольше встроенного, а «быстрое» округление вверх — на 10 % быстрее встроенного. 

Наверняка как-то можно оптимизировать вычисление истинности условий, избавиться от лишнего вызова функции (оставив  дважды повторяющийся код), но как установить, целое число или нет без операции взятия остатка или тех же встроенных в ядро джаваскрипт математических функций, я не знаю.

В самое ближайшее время я попробую разобраться, как работают битовые операции в джаваскрипте (в частности ~ — операция побитового не, т.е. инвертирования всех разрядов числа, — рабтотает немного не так, как я предполагал), а также можно ли всё-таки написать быстрое округление вверх?

  • 1
Интересно. Можно использовать в каких-нибудь графических движках. Но заметил разницу в только в фаерфоксе http://jsperf.com/math-floor-vs-math-round-vs-parseint

  • 1
?

Log in

No account? Create an account