Это очень опасно из-за показанного здесь побочного эффекта, в котором x
и y
изменены, даже если вы только передаете x
в функцию Rcpp
> x <- y <- 1:10
> assign_int(x, 1, 2)
> y
[1] 2 2 3 4 5 6 7 8 9 10
Кажется, не быстрее; для этих функций
f0 <- function(x) {
for (i in seq_along(x))
x[i] = -i
}
f1 <- function(x) {
for (i in seq_along(x))
assign_int(x, i, -i)
}
у меня
> int <- 1:10^5
> microbenchmark(f0(int), f1(int), times=5)
Unit: milliseconds
expr min lq mean median uq max neval
f0(int) 14.78777 14.80264 15.05683 15.03138 15.17678 15.48556 5
f1(int) 659.67346 669.00095 672.93343 670.48917 676.16930 689.33429 5
В вашем тесте int[i] <- 1
, '1
' является числовым (двойным) значением, поэтому вы приводите к двойному вектору (отметьте class(int)
после назначения), вызывая полную копию. Используйте int[i] <- 1L
, чтобы правая часть была целым числом.
> int0 <- int1 <- 1:10^7
> microbenchmark(int0[1] <- 1, int1[1] <- 1L)
Unit: microseconds
expr min lq mean median uq max neval
int0[1] <- 1 1.047 1.102 1770.9911 1.143 1.2650 176960.52 100
int1[1] <- 1L 1.105 1.176 339.4264 1.213 1.2655 33815.97 100
> class(int0)
[1] "numeric"
> class(int1)
[1] "integer"
Обновление только одного элемента в качестве эталона является дорогостоящим в базе R, потому что оно вызывает копию вектора при каждом назначении; в f0()
копия происходит только один раз. На первой итерации R делает копию, потому что знает, что на вектор целочисленных значений ссылаются как минимум два символа (аргумент функции int
и символ, используемый в функции x
), поэтому он создает копия памяти и присваивает ее x
внутри функции. Это делается для того, чтобы избежать побочного эффекта, видимого в вашем коде Rcpp (то есть, чтобы избежать изменения int
). В следующий раз в цикле R распознается, что только один символ ссылается на вектор, поэтому замена выполняется без копирования.
Обратите внимание, что
> int <- 1:10^5
> f1(int)
> head(int)
[1] -1 -2 -3 -4 -5 -6
иллюстрирует тонкий способ, которым побочные эффекты вашего кода Rcpp могут иметь неожиданные результаты.
Также есть несколько способов замедления итерационных циклов, например,
f2 <- function(x) {
for (i in seq_along(x)) {
x[i] = -i
y <- x
}
}
f3 <- function(x) {
result <- integer()
for (i in seq_along(x))
result <- c(result, -i)
}
с
> int <- 1:10^3
> microbenchmark(f0(int), f2(int), f3(int), times = 1)
Unit: microseconds
expr min lq mean median uq max neval
f0(int) 150.507 150.507 150.507 150.507 150.507 150.507 1
f2(int) 667.201 667.201 667.201 667.201 667.201 667.201 1
f3(int) 4379.005 4379.005 4379.005 4379.005 4379.005 4379.005 1
f2()
заставляет R делать копию x
каждый раз через цикл (чтобы избежать побочного эффекта изменения y
). f3()
копирует вектор длиной 0, 1, 2, 3, ... n - 1 (где n = length(x)
) на последовательных итерациях, что приводит к копированию n * (n - 1) / 2
элементов, и алгоритм, который масштабируется как квадрат длина x
.
Общий принцип применим и к другим типам, включая списки с
f0l <- function(x) {
for (i in seq_along(x))
x[[i]] <- i
x
}
f1l <- function(x) {
for (i in seq_along(x))
assign_list(x, i, i)
}
ведет к
> set.seed(123)
> l0 <- lapply(sample(5:20, 10^6, replace = T), rnorm, mean = 1000, sd = 300)
> set.seed(123)
> l1 <- lapply(sample(5:20, 10^6, replace = T), rnorm, mean = 1000, sd = 300)
> microbenchmark(f0l(l0), f1l(l1), times=1)
Unit: milliseconds
expr min lq mean median uq max neval
f0l(l0) 239.9865 239.9865 239.9865 239.9865 239.9865 239.9865 1
f1l(l1) 6767.9172 6767.9172 6767.9172 6767.9172 6767.9172 6767.9172 1