我最近一直致力于迭代算法,其中每次迭代n
都依赖于迭代n-1
。在每次迭代期间,大部分计算时间是通过子设置和/或替换向量,列表或data.tables(N> 10 ^ 6)的元素来进行的。
我最近遇到了Rcpp并且稍微玩了一下我发现更换向量或列表的元素k
可以加速两到三个数量级(下面几个基准测试)。
但是,当在for和while循环中使用Rcpp子集代码时,R似乎变得不稳定,并且会话在随机点中止而没有出现什么问题。
我的问题:这种使用Rcpp是合法的还是会导致我不知道的问题?
下面是我使用的Rcpp代码和一些基准测试。总的来说,算法应该将替换函数调用~55亿次,子集函数调用~500亿次。
注意,使用Rcpp替换列表和双向量的元素更快,而对于整数向量,基本R解是优选的(基准1);数据表是替换元素的好选项,但如果必须重复子集以访问其元素,则向量方法要快得多(基准2)。
功能:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
void assign_list(List x, int k, NumericVector new_element){
x[k-1] = new_element;
}
// [[Rcpp::export]]
void assign_dbl(NumericVector x, int k, double y){
x[k-1] = y;
}
// [[Rcpp::export]]
void assign_int(IntegerVector x, int k, int y){
x[k-1] = y;
}
基准:
输入
set.seed(747474)
int <- 1:10^7
dou <- rnorm(10^7, 1000, 300)
l <- lapply(sample(5:20, 10^7, replace = T), rnorm, mean = 1000, sd = 300)
dt <- data.table(int = int, dou = dou, l = l)
i <- 999999
z <- 2222
k <- 30000
s <- 552877
1)
Unit: nanoseconds
expr min lq mean median uq max neval
int[i] <- -1L 488 2439 36938108.9 4146.0 15651119 799720107 30
dou[i] <- -1 732 3170 19101960.4 6609193.5 16187500 212369197 30
l[i] <- -1 489 3902 159442538.1 186035314.5 227131872 618326686 30
assign_int 19853910 22028692 81055363.5 24665494.0 39352345 872241539 30
assign_dbl 1220 5852 48023.2 8534.5 13167 1158828 30
assign_list 1464 6828 52866.9 10850.5 13411 1243430 30
dt[k, ':=' (int = -1, dou = -1, l = -1)] 206020 340116 481850.0 425326.5 529312 1414341 30
2)
microbenchmark(times = 30L,
"subset vector + list" = {int[s]; dou[s]; l[s]},
"subset datatable" = {dt[s, int]; dt[s, dou]; dt[s, l]})
Unit: nanoseconds
expr min lq mean median uq max neval
subset vector + list 488 488 1715.533 1585.5 2926 4389 30
subset datatable 563688 574417 719304.467 600138.0 875765 1308040 30
这是非常危险的,因为这里显示的副作用,即使你只将x
传递给Rcpp函数,y
和x
也会改变
> 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