在迭代算法中使用Rcpp加速替换列表和向量的元素是否合法?

问题描述 投票:0回答:1

Context

我最近一直致力于迭代算法,其中每次迭代n都依赖于迭代n-1。在每次迭代期间,大部分计算时间是通过子设置和/或替换向量,列表或data.tables(N> 10 ^ 6)的元素来进行的。

我最近遇到了Rcpp并且稍微玩了一下我发现更换向量或列表的元素k可以加速两到三个数量级(下面几个基准测试)。

但是,当在for和while循环中使用Rcpp子集代码时,R似乎变得不稳定,并且会话在随机点中止而没有出现什么问题。

Question

我的问题:这种使用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
c++ r bigdata subset rcpp
1个回答
5
投票

这是非常危险的,因为这里显示的副作用,即使你只将x传递给Rcpp函数,yx也会改变

> 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
© www.soinside.com 2019 - 2024. All rights reserved.