- PVSM.RU - https://www.pvsm.ru -
Писать тесты скорости JS не так легко, как кажется. Даже не касаясь вопросов кроссбраузерной совместимости, можно попасть во множество ловушек.
Именно поэтому я и сделал jsPerf [1]. Простой веб-интерфейс для того, чтобы каждый мог создавать и делиться тестами, и проверять быстродействие различных фрагментов кода. Ни о чём не нужно беспокоиться – просто вводите код, быстродействие которого необходимо измерить, и jsPerf создаст для вас новую задачу по тестированию, которую вы затем сможете запустить на разных устройствах и в разных браузерах.
За кулисами jsPerf сначала использовал библиотеку на JSLitmus [2], которую я обозвал Benchmark.js [3]. Со временем она обрастала новыми возможностями, и недавно Джон-Дэвид Дальтон [4]переписал всё с нуля.
Эта статья проливает свет на разные каверзные ситуации, которые могут случиться при разработке тестов JS.
Есть несколько способов запустить тест части JS-кода для проверки на быстродействие. Самый распространённый вариант, шаблон А:
var totalTime,
start = new Date,
iterations = 6;
while (iterations--) {
// Здесь идёт фрагмент кода
}
// totalTime → количество миллисекунд, потребовавшихся на шестикратное выполнение кода
totalTime = new Date - start;
Тестируемый код размещается в цикле, который выполняется заданное количество раз (6). После этого дата старта вычитается из даты окончания. Такой шаблон используют тестировочные фреймворки SlickSpeed [5], Taskspeed [6], SunSpider [7]и Kraken [8].
При постоянном повышении быстродействия устройств и браузеров, тесты, использующие фиксированное количество повторений, всё чаще выдают 0 ms как результат работы, что нам не нужно.
Второй подход – посчитать, сколько операций совершается за фиксированное время. Плюс: не нужно выбирать количество итераций.
var hz,
period,
startTime = new Date,
runs = 0;
do {
// Здесь идёт фрагмент кода
runs++;
totalTime = new Date - startTime;
} while (totalTime < 1000);
// преобразуем ms в секунды
totalTime /= 1000;
// period → сколько времени занимает одна операция
period = totalTime / runs;
// hz → количество операций в секунду
hz = 1 / period;
// или можно записать короче
// hz = (runs * 1000) / totalTime;
Выполняет код примерно секунду, т.е. пока totalTime не превысит 1000 ms.
Шаблон B используется в Dromaeo [9]и V8 Benchmark Suite [10].
Из-за сборки мусора, оптимизаций движка и других фоновых процессов время выполнения одного и того же кода может меняться. Поэтому тест желательно запускать много раз и усреднять результаты. V8 Suite запускает тесты только один раз. Dromaeo – по пять раз, но иногда этого недостаточно. Например, уменьшить минимальное время выполнения теста с 1000 до 50 ms, чтобы больше времени оставалось на повторенные запуски.
JSLitmus комбинирует два шаблона. Он использует шаблон А для прогона теста в цикле n раз, но циклы адаптируются и увеличивают n во время выполнения, пока не наберётся минимальное время выполнения теста – т.е. как в шаблоне В.
JSLitmus избегает проблем шаблона А, но от проблем шаблона В не уходит. Для калибровки выбираются 3 самых быстрых повторения теста, которые вычитаются из результатов остальных. К сожалению, «лучший из трёх» — статистически не лучший метод. Даже если прогнать тесты много раз и вычесть калибровочное среднее из среднего результата, увеличившаяся погрешность полученного результата съест всю калибровку.
Проблемы предыдущих шаблонов можно исключить через компиляцию функций и развёртку циклов.
function test() {
x == y;
}
while (iterations--) {
test();
}
// …скомпилируется в →
var hz,
startTime = new Date;
x == y;
x == y;
x == y;
x == y;
x == y;
// …
hz = (runs * 1000) / (new Date - startTime);
Но и здесь есть недостатки. Компиляция функций увеличивает используемую память и замедляет работу. При повторении теста несколько миллионов раз вы создаёте очень длинную строку и компилируете гигантскую функцию.
Ещё одна проблема с развёрткой цикла – тест может организовать выход через return в начале работы. Нет смысла компилировать миллион строк, если функция делает возврат на третьей строчке. Нужно отслеживать эти моменты и пользоваться шаблоном А в таких случаях.
В Benchmark.js используется другая технология. Можно сказать, что она включает лучшие стороны всех этих шаблонов. Мы не развёртываем циклы для экономии памяти. Чтоб уменьшить факторы, влияющие на точность, и разрешить тестам работать с локальными методами и переменными, мы извлекаем для каждого теста тело функции. К примеру:
var x = 1,
y = '1';
function test() {
x == y;
}
while (iterations--) {
test();
}
// …скомпилируется в →
var x = 1,
y = '1';
while (iterations--) {
x == y;
}
После этого мы запускаем извлечённый код в цикле while (шаблон А), повторяем до тех пор, пока не пройдёт заданное время (шаблон В), повторяем всё вместе столько раз, чтобы получить статистически значимые результаты.
В некоторых комбинациях ОС и браузера таймеры могут работать неверно по разным [11]причинам [12]. Например, при загрузке Windows XP время прерывания обычно составляет 10-15 мс. То есть, каждые 10 мс ОС получает прерывание от системного таймера. Некоторые старые браузеры (IE, Firefox 2) полагаются на таймер ОС, то есть, например, вызов Date().getTime() получает данные непосредственно от операционки. И если таймер обновляется только каждые 10-15 мс, это приводит к накоплению неточностей измерения.
Однако, это можно обойти. В JS можно получить минимальную единицу измерения времени [13]. После этого нужно рассчитать [14]время работы теста так, чтобы погрешность составляла не более 1%. Для получения погрешности нужно поделить эту минимальную единицу пополам. Например, мы используем IE6 на Windows XP и минимальная единица – 15 мс. Погрешность составляет 15 ms / 2 = 7.5 ms. Чтобы эта погрешность составляла не более 1% от времени измерения, поделим её на 0.01: 7.5 / 0.01 = 750 ms.
При запуске с параметром --enable-benchmarking flag, Chrome и Chromium дают доступ к методу chrome.Interval, который позволяет использовать таймер высокого разрешения вплоть до микросекунд. При работе над Benchmark.js Джон-Дэвид Дальтон встретил в Java наносекундный таймер [15], и сделал доступ к нему из JS через небольшой java-applet [16].
Используя таймер высокого разрешения, можно задавать меньшее время теста, что даёт меньше ошибок в результате.
Запущенный аддон Firebug отключает встроенную компиляцию по системе just-in-time, [17]поэтому все тесты выполняются в интерпретаторе. Они будут работать там гораздо медленнее, чем обычно. Не забывайте отключать Firebug перед тестами.
То же, хотя и в меньшей степени, касается Web Inspector и Opera’s Dragonfly. Закрывайте их перед запуском тестов, чтобы они не влияли на результаты.
Тесты, использующие циклы, подвержены различным багам браузеров – пример был продемонстрирован в IE9 с его функцией удаления «мёртвого кода [18]». Баги в движке Mozilla TraceMonkey [19]или кеширование результатов querySelectorAll в Opera 11 [20]тоже могут помешать получению правильных результатов. Нужно иметь их в виду.
В статье Джона Резига [21]описано, почему большинство тестов не выдают статистически значимые результаты. Короче говоря, нужно всегда оценивать величину ошибки каждого результата и уменьшать её всеми возможными способами.
Тестируйте скрипты на реальных разных версиях браузеров. Не полагайтесь, например, на режимы совместимости [22]в IE. Также, IE вплоть до 8-й версии ограничивал работу скрипта 5 миллионами инструкций. Если ваша система быстрая, то скрипт может выполнить их и за полсекунды. В этом случае вы получите сообщение “Script Warning” в браузере. Тогда придётся подредактировать количество разрешённых операций в реестре. Или воспользоваться программкой [23], исправляющей это ограничение. К счастью, в IE9 его уже убрали
Заключение
Выполняете ли вы несколько тестов, пишете ли свой набор тестов или даже библиотеку – в вопросе тестирования JS есть много скрытых моментов. Benchmark.js и jsPerf обновляются еженедельно [24], исправляют баги и добавляют новые возможности, повышая точность тестов.
Автор: SLY_G
Источник [25]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/81971
Ссылки в тексте:
[1] jsPerf: http://jsperf.com/
[2] JSLitmus: http://www.broofa.com/Tools/JSLitmus/
[3] Benchmark.js: http://benchmarkjs.com/
[4] Джон-Дэвид Дальтон : http://allyoucanleet.com/
[5] SlickSpeed: https://github.com/kamicane/slickspeed/
[6] Taskspeed: https://github.com/phiggins42/taskspeed
[7] SunSpider : http://www2.webkit.org/perf/sunspider/sunspider.html
[8] Kraken: http://krakenbenchmark.mozilla.org/
[9] Dromaeo : http://dromaeo.com/
[10] V8 Benchmark Suite: https://code.google.com/apis/v8/benchmarks.html
[11] разным : http://msdn.microsoft.com/en-us/windows/hardware/gg463347.aspx
[12] причинам: http://alivebutsleepy.srnet.cz/unreliable-system-nanotime/
[13] минимальную единицу измерения времени: https://mathiasbynens.be/demo/javascript-timers
[14] рассчитать : http://spiff.rit.edu/classes/phys273/uncert/uncert.html
[15] Java наносекундный таймер: http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#nanoTime%28%29
[16] небольшой java-applet: https://github.com/bestiejs/benchmark.js/blob/master/nano.java#files
[17] just-in-time, : http://en.wikipedia.org/wiki/Just-in-time_compilation
[18] мёртвого кода: http://www.zdnet.com/blog/bott/ie9-takes-top-benchmark-prize-no-cheating-involved/2671
[19] Mozilla TraceMonkey : https://bugzilla.mozilla.org/show_bug.cgi?id=509069
[20] querySelectorAll в Opera 11 : http://jsperf.com/jquery-css3-not-vs-not
[21] статье Джона Резига : http://ejohn.org/blog/javascript-benchmark-quality/
[22] режимы совместимости : http://jsperf.com/join-concat#comments
[23] программкой: http://go.microsoft.com/?linkid=9729250
[24] еженедельно: https://github.com/bestiejs/benchmark.js/commits/master
[25] Источник: http://habrahabr.ru/post/249969/
Нажмите здесь для печати.