R:Измерение времени выполнения выражений
|
Материал «R:Измерение времени выполнения выражений», созданный автором Артём Клевцов, публикуется на условиях лицензии Creative Commons «Attribution» («Атрибуция») 4.0 Всемирная. | |
|
Перед использованием функций из пакетов их необходимо предварительно установить и загрузить: КодR <syntaxhighlight lang="r">> install.packages(pkgs = "pkgname") > library(package = "pkgname")</syntaxhighlight> |
Для измерения времени выполнения выражений используют следующие инструменты:
- Функция
system.time()
из базового пакета; - Cпециализированные функции (benchmarks, бенчмарки);
- Функция профилирования времени выполнения кода
Rprof()
.
Функция system.time
Самый простой инструмент для измерения времени выполнения кода - функция system.time()
из пакета base
. В качестве аргумента функция system.time()
принимает выражения и возвращает время выполнения данного выражения. Измерим время выполнения функции Sys.sleep()
, которая останавливает выполнение кода на заданный интервал времени (в секундах):
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Как видим, на выполнение данной операции заняло ровно одну секунду.
Приведём ещё один пример. Сравним время вычисления встроенной в R функции mean()
и среднего, вычисленного по формуле [math]\frac{1}{n}\sum_{i=1}^{n}x_{i}[/math]. на сгенерированном массиве нормально распределенных значений:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Функция возвращает 3 значения:
-
user
- время CPU, которое занял пользователь; -
system
- время CPU, которое заняла система; -
elapsed
- реальное время, которое заняло выполнение команды.
Соответственно, в выводе функции нас интересует значение elapsed
, который показывает время выполнения функции (выражения) в секундах. Как мы видим, внешне более сложное выражение sum(x) / length(x)
выполняется быстрее стандартной функции mean(x)
.
К сожалению, подобный способ достаточно ненадежен, так как для оценки времени выполнения выражения функция system.time()
обращается к системным значениям времени. Следовательно, если во время выполнения кода параллельно производятся и другие операции на компьютере (а такое случается практически в ста процентах случаев), то возможно увеличение времени выполнения R-кода. Некоторую вариативность результатов можно увидеть, даже если выполнить функцию system.time()
несколько раз подряд. Подобной неточности оценки можно избежать путём многократного повторения выполняемых выражений и вычислением среднего времени, что позволит сгладить часть вариаций.
Проиллюстрируем вышесказанное на примере:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
В этом примере с помощью функции replicate()
мы повторили выражение system.time(mean(x))
10 раз, отфильтровав вывод функции system.time()
так, чтобы нам выводилось только время выполнения команды, дописав "elapsed"
. Как мы видим, время выполнения при повторном выполнении выражения может отличаться.
Базовый пакет позволяет реализовать процедуру многократного повторения выражения функции как минимум двумя способами. Первый - функция replicate()
. Приведенное выше сопоставление времени выполнения двух выражений при использовании функции replicate()
будет выглядеть следующим образом:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Тот же самый эффект можно получить и с помощью обычного цикла for()
:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Можно также использовать описательные статистики в сочетании с множественными повторениями:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
В примере выше мы взяли только значения elapsed
и рассчитали медиану [1].
Вместо подобных решений можно использовать специальные пакеты, предназначенные для измерения производительности кода, в частности, пакеты rbenchmark
и microbenchmark
. Основной принцип работы этих пакетов заключается в многократном выполнении выражений и расчёта ряда интегральных показателей, в частности, суммы, среднего значения или медианы времени выполнения всех попыток.
Пакет rbenchmark
Основа пакета rbenchmark
- функция benchmark()
. Данная функция работает следующим образом: указанные в качестве аргументов выражения выполняются заданное количество раз (по умолчанию 100) и вычисляется время, затраченное на выполнение всех попыток. В качестве аргументов функции benchmark()
необходимо передать выражения или функции, а также количество повторений, передаваемых аргументом replications[2].
Для примера возьмём несколько способов расчёта среднего арифметического для сгенерированного массива данных.
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Использованные нами способы - функции векторизованных вычислений (apply()
, vapply()
), стандартный цикл и специальная функция вычисления средних по столбцам ColMeans()
. Представим эти способы в виде самостоятельных функций для удобства их вызова при работе с benchmark()
:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Убедимся, что функции возвращают одинаковый результат. Сделать это можно с помощью функций identical()
или all.equal()
:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Теперь, подключив пакет rbenchmark, мы можем сравнить время работы каждого из выбранных нами способов вычисления средних по столбцам:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Наиболее важны для нас в выводе функции benchmark()
столбцы elapsed
и relative
. Столбец elapsed
показывает время в секундах, затраченное на выполнение интересующей нас функции. Как видим из примера, самыми медленными оказались функции colMeansApply()
и colMeansLoop()
, а самой быстрой colMeans()
, причём превосходит остальные по скорости выполнения как минимум в 7 раз.
Показатель relative
дает информацию о разнице во времени относительно самого быстрого выражения (в нашем случае это ColMeans()
), т.е. время самого быстрого выражения берётся за единицу, и рассчитывается относительное время для остальных выражений.
Для более удобного просмотра можно отфильтровать вывод функции benchmark()
с помощью аргумента columns
. Также может быть полезен аргумент order
, позволяющий отсортировать вывод по любому из столбцов. Для примера зададим набор показателей, которые мы хотим включить в таблицу (в данном случае это «test», «replications», «elapsed», «relative»), и отсортируем выдачу по столбцу «elapsed» по возрастанию значений:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Таким образом, сравнив несколько альтернатив решения нашей задачи, мы можем сделать обоснованный выбор в пользу наиболее эффективного варианта.
Чтобы не указывать нужные столбцы каждый раз, когда используется функция benchmark()
, можно закрепить заданный формат выдачи результатов (далее используется именно такой формат вывода, с сортировкой по столбцу «relative»). Для этого следует воспользоваться функцией formals()
:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Пакет microbenchmark
Функция microbenchmark()
одноименного пакета работает сходным с функцией benchmark()
образом, но предоставляет более гибкие средства по управлению процессом выполнения выражений[3]. Особенностями реализованных в пакете microbenchmark
являются:
- Возможность измерения времени выполнения выражения вплоть до наносекунд;
- Возможность контролировать последовательность выполнения выражений: случайно или последовательно;
- Возможность проведения предварительных испытаний до начала процесса измерений.
Также с помощью функции microbenchmark()
можно получить исходную информацию о времени выполнения каждой попытки, что даёт достаточно широкие возможности по обработке и анализу полученных результатов.
В таблице ниже представлено время выполнения пяти функций вычисления среднего значения из предыдущего примера, полученное с помощью функции microbenchmark()
:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Все результаты представлены в виде описательных статистик, рассчитанных из времени выполнения каждой попытки. Наиболее информативный столбец - это столбец median, который показывает медиану времени выполнения выражения для всех попыток.
Вся полученная информация о попытках применения функций вычисления средних записана в отдельную переменную res
. С помощью функции str()
можно увидеть структуру переменной:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Переменная res
, как можно увидеть в выводе функции str()
, представляет собой список (list) и включает в себя две переменные: expr
(выражение) и time
(время выполнения). На основе этой информации и рассчитываются описательные статистики, приведённые в примере применения функции microbenchmark()
. Наличие исходных данных о каждой попытке позволяет самостоятельно выбирать, рассчитывать и сравнивать предпочтитаемые показатели. Например, расчет медианного времени выполнения попытки и общего времени выполнения всех попыток для каждого выражения выглядит следующим образом:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Умножение на [math]10^{-6}[/math] --- это перевод в миллисекунды. Чтобы получить секунды, нужно, соответственно, умножить на [math]10^{-9}[/math].
Помимо настройки формата вывода, выбора показателей, наличие информации о времени выполнения выражения в каждой попытке позволяет визуализировать результаты оценки времени выполнения выражения. Например, с помощью функции autoplot()
из пакета ggplot2
, можно получить следующий график:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Ещё один довольно интересный способ графического представления результатов измерения скорости выполнения кода с помощью функции qplot()
представлен ниже:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Так же можно, если возникнет необходимость, оценить статистическую значимость различий во времени выполнения выражений. Благодаря тому, что в переменной res
хранятся данные о времени выполнения каждой попытки из заданного числа, становится возможным использование статистических критериев. Выбор критерия - на усмотрение аналитика, в примере ниже использовался параметрический критерий сравнения групп t-Стьюдента с поправкой уровня статистической значимости Холма для множественных сравнений:
<syntaxhighlight lang="r">{{{code}}}</syntaxhighlight>
Из вышеприведённого вывода видно, что скорость выполнения всех функций статистически значимо разлиается у всех функций, за исключением пары colMeansLoop(x)
- colMeansVapply(x)
(p-уровень = 0.62).
Примечания
- ↑ Медиана является более устойчивой мерой центральной тенденции при асимметрии распределения, что, как правило, характерно для измерения времени.
- ↑ Анализ функции
benchmark()
показал, что, данная функция используетsystem.time()
иreplicate()
, рассмотренные в предыдущем разделе. - ↑ Но в отличии от функции benchmark() использует собственную реализацию измерения времени выполнения и организацию повторных испытаний.
Ссылки
- Olaf Mersmann (2013). microbenchmark: Sub microsecond accurate timing functions.. R package version 1.3-0.
- Wacek Kusnierczyk (2012). rbenchmark: Benchmarking routine for R. R package version 1.0.0.