【问题标题】:Why is the Rcpp implementation in my example much slower than the R function?为什么我的示例中的 Rcpp 实现比 R 函数慢得多?
【发布时间】:2019-08-28 13:25:53
【问题描述】:

我在 C++ 和 R 方面有一些经验,但我是 Rcpp 的新手。最近我在之前的一些项目中使用 Rcpp 取得了巨大的成功,因此决定将它应用到一个新项目中。我很惊讶我的 Rcpp 代码可能比相应的 R 函数慢得多。我试图简化我的 R 函数以找出原因,但找不到任何线索。非常欢迎您的帮助和 cmets!

比较 R 和 Rcpp 实现的主要 R 函数:

main <- function(){

  n <- 50000
  Delta <- exp(rnorm(n))
  delta <- exp(matrix(rnorm(n * 5), nrow = n))
  rx <- matrix(rnorm(n * 20), nrow = n)
  print(microbenchmark(c1 <- test(Delta, delta, rx), times = 500))
  print(microbenchmark(c2 <- rcpp_test(Delta, delta, rx), times = 500))

  identical(c1, c2)
  list(c1 = c1, c2 = c2)
}

R 实现:

test <- function(Delta, delta, rx){

  const <- list()
  for(i in 1:ncol(delta)){
    const[[i]] <- rx * (Delta / (1 + delta[, i]))
  }

  const

}

Rcpp 实现:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
List rcpp_test(NumericVector Delta, 
               NumericMatrix delta, 
               NumericMatrix rx) {

  int n = Delta.length();
  int m = rx.ncol();

  List c; 
  NumericMatrix c1;
  for(int i = 0; i < delta.ncol(); ++i){
    c1 = NumericMatrix(n, m);
    for(int k = 0; k < n; ++k){
      double tmp = Delta[k] / (1 + delta(k, i));
      for(int j = 0; j < c1.ncol(); ++j){
        c1(k, j) = rx(k, j) * tmp; 
      }
    }
    c.push_back(c1);
  }

  return c;

}

我知道使用 Rcpp 并不能保证提高效率,但是鉴于我在这里展示的简单示例,我不明白为什么 Rcpp 代码运行如此缓慢。

Unit: milliseconds
                         expr      min       lq     mean   median       uq      max neval
 c1 <- test(Delta, delta, rx) 13.16935 14.19951 44.08641 30.43126 73.78581 115.9645   500
Unit: milliseconds
                              expr      min       lq     mean  median       uq      max neval
 c2 <- rcpp_test(Delta, delta, rx) 143.1917 158.7481 171.6116 163.413 173.7677 247.5495   500

理想情况下,rx 是我项目中的矩阵列表。 for 循环中的变量i 将用于选择一个元素进行计算。一开始我怀疑将List 传递给Rcpp 可能会有很高的开销,所以在这个例子中,我假设rx 是一个用于所有i 的固定矩阵。似乎这不是缓慢的原因。

【问题讨论】:

  • 在 C++ 中,push_back() 在性能方面可能会花费很多,在需要快速执行速度的应用程序中应避免使用。最好事先分配所需的内存。
  • @RHertel 感谢您的评论。但在这个例子中,像 Ralf Stubner 所做的那样分配列表 c 并没有多大帮助。请参考我对 Ralf Stubner 的回答。
  • 请注意,我确实 not 将我的评论作为对您问题的回答,因为我确实 not 声明push_back() 的使用是代码速度低的主要原因。我说的是push_back()可以消耗相当多的性能,最好在开始时分配内存。此外,我认为指出在循环中动态增长对象是不好的编程风格是有用的。

标签: r rcpp


【解决方案1】:

您的 R 代码似乎或多或少是最优的,即所有实际工作都在编译后的代码中完成。对于 C++ 代码,我能找到的主要问题是在紧密循环中调用 c1.ncol()。如果我用m 替换它,C++ 解决方案几乎和 R 一样快。如果我将 RcppArmadillo 添加到混合中,我会得到一个非常紧凑的语法,但不会比纯 Rcpp 代码快。对我来说,这表明很难击败编写良好的 R 代码:

//  [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>
using namespace Rcpp;

// [[Rcpp::export]]
List arma_test(const arma::vec& Delta,
           const arma::mat& delta,
           const arma::mat& rx) {
  int l = delta.n_cols;
  List c(l);

  for (int i = 0; i < l; ++i) {
    c(i) = rx.each_col() % (Delta / (1 + delta.col(i)));
  }

  return c;  
}

// [[Rcpp::export]]
List rcpp_test(NumericVector Delta, 
               NumericMatrix delta, 
               NumericMatrix rx) {

  int n = Delta.length();
  int m = rx.ncol();

  List c(delta.ncol()); 
  NumericMatrix c1;
  for(int i = 0; i < delta.ncol(); ++i){
    c1 = NumericMatrix(n, m);
    for(int k = 0; k < n; ++k){
      double tmp = Delta[k] / (1 + delta(k, i));
      for(int j = 0; j < m; ++j){
        c1(k, j) = rx(k, j) * tmp; 
      }
    }
    c(i) = c1;
  }

  return c;

}

/*** R
test <- function(Delta, delta, rx){

  const <- list()
  for(i in 1:ncol(delta)){
    const[[i]] <- rx * (Delta / (1 + delta[, i]))
  }

  const

}

n <- 50000
Delta <- exp(rnorm(n))
delta <- exp(matrix(rnorm(n * 5), nrow = n))
rx <- matrix(rnorm(n * 20), nrow = n)
bench::mark(test(Delta, delta, rx),
            arma_test(Delta, delta, rx),
            rcpp_test(Delta, delta, rx))
 */

输出:

# A tibble: 3 x 14
  expression     min    mean  median     max `itr/sec` mem_alloc  n_gc n_itr
  <chr>      <bch:t> <bch:t> <bch:t> <bch:t>     <dbl> <bch:byt> <dbl> <int>
1 test(Delt…  84.3ms  85.2ms  84.9ms  86.6ms     11.7     44.9MB     2     4
2 arma_test… 106.5ms 107.7ms 107.7ms 108.9ms      9.28    38.1MB     3     2
3 rcpp_test… 101.9ms 103.2ms 102.2ms 106.6ms      9.69    38.1MB     1     4
# … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#   time <list>, gc <list>

我还明确地将输出列表初始化为所需的大小,避免使用push_back,但这并没有太大的区别。不过,对于来自 Rcpp 的向量类数据结构,您绝对应该避免使用 push_back,因为每次扩展向量时都会生成一个副本。

【讨论】:

  • 避免在 for 循环中调用 c1.ncol() 有很大帮助!这使我的 Rcpp 代码比 R 代码效率高 50%。我认为这可能取决于平台(我在 Mac 上测试过),因为在您的基准测试中,Rcpp 函数比 R 函数稍慢。
  • 这可能是题外话,但如果可以的话,我也会在这个线程中问它。 @RHertel 和您都建议避免使用push_back。我的问题是我们知道列表c 的大小,但不知道每个矩阵元素的大小。在这种情况下,我猜想将 List 初始化为 List c(delta.ncol()) 无法分配足够的内存。如果我们知道c 的每个元素的大小,有没有什么方法可以更有效地初始化c?其实c(i)rx[i]的维度是一样的。
  • @HanZhang 有趣。我只是在另一台(速度更快但也是 Linux)机器上重新运行基准测试,相对结果保持不变。但是“YMMV”是一个应该添加到每个基准测试中的东西...... ;-) 至于为列表分配内存:初始化列表时,它包含所需数量的指针(指向 SEXP,R 的联合数据类型)。实际数据的内存是单独分配的,但这应该不是问题。
【解决方案2】:

我想补充一下@RalfStubner 的出色回答。

您会注意到我们在第一个 for 循环中进行了许多分配(即c1 = NumerMatrix(n, m))。这可能会很昂贵,因为除了分配内存之外,我们还将每个元素初始化为 0。我们可以将其更改为以下内容以提高效率:

NumericMatrix c1 = no_init_matrix(n, m)

我还继续在可能的情况下添加了关键字const。这样做是否允许编译器优化某些代码段是值得商榷的,但我仍然在我可以添加的地方添加它以使代码清晰(即“我不希望这个变量改变”)。有了这个,我们有:

// [[Rcpp::export]]
List rcpp_test_modified(const NumericVector Delta, 
                        const NumericMatrix delta, 
                        const NumericMatrix rx) {

    int n = Delta.length();
    int m = rx.ncol();
    int dCol = delta.ncol();

    List c(dCol);

    for(int i = 0; i < dCol; ++i) {
        NumericMatrix c1 = no_init_matrix(n, m);

        for(int k = 0; k < n; ++k) {
            const double tmp = Delta[k] / (1 + delta(k, i));

            for(int j = 0; j < m; ++j) {
                c1(k, j) = rx(k, j) * tmp; 
            }
        }

        c[i] = c1;
    }

    return c;

}

这里有一些基准(Armadillo 解决方案省略):

bench::mark(test(Delta, delta, rx),
            rcpp_test_modified(Delta, delta, rx),
            rcpp_test(Delta, delta, rx))
# A tibble: 3 x 14
  expression     min   mean  median    max `itr/sec` mem_alloc  n_gc n_itr total_time result memory time 
  <chr>      <bch:t> <bch:> <bch:t> <bch:>     <dbl> <bch:byt> <dbl> <int>   <bch:tm> <list> <list> <lis>
1 test(Delt… 12.27ms 17.2ms 14.56ms 29.5ms      58.1    41.1MB    13     8      138ms <list… <Rpro… <bch…
2 rcpp_test…  7.55ms 11.4ms  8.46ms   26ms      87.8    38.1MB    16    21      239ms <list… <Rpro… <bch…
3 rcpp_test… 10.36ms 15.8ms 13.64ms 28.9ms      63.4    38.1MB    10    17      268ms <list… <Rpro… <bch…
# … with 1 more variable: gc <list>

我们看到50% 改进了Rcpp 版本。

【讨论】:

  • 您使用 no_init_matrix 的技巧很棒。在我的应用程序中,矩阵中的许多条目将为零,我只会将值分配给某些列。不过,将来会使用这个技巧。
猜你喜欢
  • 1970-01-01
  • 2019-01-14
  • 2019-11-16
  • 1970-01-01
  • 1970-01-01
  • 2018-04-18
  • 2022-12-20
  • 2021-09-09
相关资源
最近更新 更多