R Language
Ускорение жестко-векторизованного кода
Поиск…
Ускорение жесткой векторизации для циклов с Rcpp
Возьмем следующий цикл for-loopize, который создает вектор длины len
где указан первый элемент ( first
), и каждый элемент x_i
равен cos(x_{i-1} + 1)
:
repeatedCosPlusOne <- function(first, len) {
x <- numeric(len)
x[1] <- first
for (i in 2:len) {
x[i] <- cos(x[i-1] + 1)
}
return(x)
}
Этот код включает цикл for с быстрой операцией ( cos(x[i-1]+1)
), которые часто выигрывают от векторизации. Однако нетривиально векторизовать эту операцию с базой R, так как R не имеет «кумулятивного косинуса x + 1».
Одним из возможных подходов к ускорению этой функции было бы реализовать ее на C ++, используя пакет Rcpp:
library(Rcpp)
cppFunction("NumericVector repeatedCosPlusOneRcpp(double first, int len) {
NumericVector x(len);
x[0] = first;
for (int i=1; i < len; ++i) {
x[i] = cos(x[i-1]+1);
}
return x;
}")
Это часто обеспечивает значительное ускорение для больших вычислений, при этом получая точные результаты:
all.equal(repeatedCosPlusOne(1, 1e6), repeatedCosPlusOneRcpp(1, 1e6))
# [1] TRUE
system.time(repeatedCosPlusOne(1, 1e6))
# user system elapsed
# 1.274 0.015 1.310
system.time(repeatedCosPlusOneRcpp(1, 1e6))
# user system elapsed
# 0.028 0.001 0.030
В этом случае код Rcpp генерирует вектор длиной 1 миллион за 0,03 секунды вместо 1,31 секунды с базовым R-подходом.
Ускорение жесткой для векторизации для циклов путем составления байтов
Следуя примеру Rcpp в этой записи документации, рассмотрим следующую жесткую функцию для векторизации, которая создает вектор длины len
где указан первый элемент ( first
), и каждый элемент x_i
равен cos(x_{i-1} + 1)
:
repeatedCosPlusOne <- function(first, len) {
x <- numeric(len)
x[1] <- first
for (i in 2:len) {
x[i] <- cos(x[i-1] + 1)
}
return(x)
}
Один простой подход к ускорению такой функции без перезаписи одной строки кода - это байт, компилирующий код с использованием компилируемого пакета R:
library(compiler)
repeatedCosPlusOneCompiled <- cmpfun(repeatedCosPlusOne)
Результирующая функция часто будет значительно быстрее, при этом возвращая те же результаты:
all.equal(repeatedCosPlusOne(1, 1e6), repeatedCosPlusOneCompiled(1, 1e6))
# [1] TRUE
system.time(repeatedCosPlusOne(1, 1e6))
# user system elapsed
# 1.175 0.014 1.201
system.time(repeatedCosPlusOneCompiled(1, 1e6))
# user system elapsed
# 0.339 0.002 0.341
В этом случае байт-компиляция ускоряла жесткую для векторизации операцию с вектором длиной 1 миллион с 1,20 секунды до 0,34 секунды.
замечание
Сущность repeatedCosPlusOne
, как совокупное применение одной функции, может быть более прозрачно выражена с помощью Reduce
:
iterFunc <- function(init, n, func) {
funcs <- replicate(n, func)
Reduce(function(., f) f(.), funcs, init = init, accumulate = TRUE)
}
repeatedCosPlusOne_vec <- function(first, len) {
iterFunc(first, len - 1, function(.) cos(. + 1))
}
repeatedCosPlusOne_vec
может рассматриваться как «векторизация» repeatedCosPlusOne
. Однако можно ожидать, что он будет медленнее в 2 раза:
library(microbenchmark)
microbenchmark(
repeatedCosPlusOne(1, 1e4),
repeatedCosPlusOne_vec(1, 1e4)
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval cld
#> repeatedCosPlusOne(1, 10000) 8.349261 9.216724 10.22715 10.23095 11.10817 14.33763 100 a
#> repeatedCosPlusOne_vec(1, 10000) 14.406291 16.236153 17.55571 17.22295 18.59085 24.37059 100 b