Rcpp 代理模型和 R 内存分配

hsl*_*oot 5 r rcpp

我试图更好地了解 Rcpp 代理模型的工作原理。

为此,请考虑以下任务:采样指数随机变量并对结果进行处理。一个天真的 Rcpp 实现可能是

NumericMatrix rmexp1(int n, int d) {
  NumericMatrix out(n, d);
  NumericVector values;
  for (int k=0; k<n; k++) {
    values = Rcpp::rexp(d);
    // do something with values 
    out(k, _) = values;
  }
  return out;
}
Run Code Online (Sandbox Code Playgroud)

以下说法正确吗?

  • 在每次迭代中,在 l#5 中,Rcpp::rexp为新的 R 向量分配空间,然后values存储对该向量的引用并丢弃它先前持有的引用。
  • 在 l#7 中,由于左侧和右侧的数据类型不同,因此将 中的值values硬拷贝到其中out(k, _)
  • 如果是这种情况,则会为 R 中的对象分配大量内存,而没有任何实际需要。如果速度是一个问题,是否应该避免这种情况?

Ral*_*ner 6

让我们通过实验来解决这个问题。R 分配了多少内存,需要多长时间?首先,让我们使用您的函数并使用不同的参数运行它。我将其包装在 中bench::mark,因为这为我提供了 RAM 和 CPU 测量值:

> bench::mark(rmexp1(100, 10),
+             rmexp1(100, 100),
+             rmexp1(100, 1000),
+             rmexp1(100, 10000),
+             check = FALSE)
#> # A tibble: 4 x 13
#>   expression              min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#>   <bch:expr>         <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#> 1 rmexp1(100, 10)     46.93µs  52.61µs   16307.    10.35KB     8.24  7918     4
#> 2 rmexp1(100, 100)   381.41µs 538.42µs    1786.      3.9MB     4.14   863     2
#> 3 rmexp1(100, 1000)    4.83ms   5.08ms     187.     1.53MB     8.68    86     4
#> 4 rmexp1(100, 10000)  59.85ms  63.19ms      15.5   15.27MB     5.17     6     2
#> # … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#> #   time <list>, gc <list>
Run Code Online (Sandbox Code Playgroud)

不出所料,更大的矩阵需要更长的时间并需要更多的内存。此外,分配的内存大约是输出矩阵所需内存的两倍。所以是的,我们分配的内存比这里需要的多。

那性能很关键吗?这取决于。毕竟,您正在创建具有指数分布的随机变量,这需要有限的时间。此外,您正在 中进行一些未指定的计算do something with values,这可能需要更长的时间。让我们通过使用仅分配内存或不将其初始化为零的替代函数来摆脱创建随机变量:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
NumericMatrix rmzero(int n, int d) {
    NumericMatrix out(n, d);
    NumericVector values;
    for (int k=0; k<n; k++) {
        values = Rcpp::NumericVector(d);
        // do something with values 
        out(k, _) = values;
    }
    return out;
}

// [[Rcpp::export]]
NumericMatrix rmnoinit(int n, int d) {
    NumericMatrix out(n, d);
    NumericVector values;
    for (int k=0; k<n; k++) {
        values = Rcpp::NumericVector(Rcpp::no_init(d));
        // do something with values 
        out(k, _) = values;
    }
    return out;
}
Run Code Online (Sandbox Code Playgroud)

随着bench::mark我们得到:

> bench::mark(rmexp1(100, 1000),
+             rmzero(100, 1000),
+             rmnoinit(100, 1000),
+             check = FALSE)
#> # A tibble: 3 x 13
#>   expression               min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#>   <bch:expr>          <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#> 1 rmexp1(100, 1000)     4.83ms   5.05ms      190.    1.53MB     8.72    87     4
#> 2 rmzero(100, 1000)   509.74µs 562.24µs     1510.    1.53MB    60.4    525    21
#> 3 rmnoinit(100, 1000) 404.24µs 469.43µs     1785.    1.53MB    53.8    664    20
#> # … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#> #   time <list>, gc <list>
Run Code Online (Sandbox Code Playgroud)

因此,大约只有 1/10 的函数执行时间是由于内存分配和其他开销造成的。其余的来自随机变量。

如果生成随机变量是您代码中的实际瓶颈,您可能对我的dqrng包感兴趣:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::depends(dqrng)]]
#include <dqrng.h>
// [[Rcpp::export]]
NumericMatrix rmdqexp1(int n, int d) {
    NumericMatrix out(n, d);
    NumericVector values;
    for (int k=0; k<n; k++) {
        values = dqrng::dqrexp(d);
        // do something with values 
        out(k, _) = values;
    }
    return out;
}
Run Code Online (Sandbox Code Playgroud)

随着bench::mark我们得到:

> bench::mark(rmexp1(100, 1000),
+             rmdqexp1(100, 1000),
+             check = FALSE)
#> # A tibble: 2 x 13
#>   expression             min median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#>   <bch:expr>          <bch:> <bch:>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#> 1 rmexp1(100, 1000)   3.69ms 5.03ms      201.    1.53MB     6.36    95     3
#> 2 rmdqexp1(100, 1000) 1.09ms 1.21ms      700.    1.65MB    22.6    310    10
#> # … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#> #   time <list>, gc <list>
Run Code Online (Sandbox Code Playgroud)

使用更快的随机数生成器可以节省相当多的时间。