我的带有 Rcpp::List 输入的 C++ 函数非常慢

Posted

技术标签:

【中文标题】我的带有 Rcpp::List 输入的 C++ 函数非常慢【英文标题】:My C++ functions with Rcpp::List inputs are very slow 【发布时间】:2014-04-18 23:54:56 【问题描述】:

虽然 C++,特别是 Rcpp 包在加速我的代码方面对我有很大帮助,但我注意到我的 C++ 函数有一个列表或数据框输入参数(Rcpp::DataFrame 和 Rcpp:: 形式的参数) List) 与我的其他 C++ 函数相比非常慢。我写了一个示例代码,我想请教一些可以让我的代码更快的技巧:

首先,让我们在 R 中模拟一个 List,其中包含两个 List。将 myList 视为包含两个列表 - measure1 和 measure2 的列表。 measure1 和 measure2 本身就是列表,每个列表都包含对象的测量向量。这是R代码:

lappend <- function(lst, ...)
  lst <- c(lst, list(...))
return(lst)


nSub <- 30
meas1 <- list()
meas2 <- list()
for (i in 1:nSub)
  meas1 <- lappend(meas1, rnorm(10))
  meas2 <- lappend(meas2, rnorm(10))

myList <- list(meas1 = meas1, meas2 = meas2)

现在,假设我想要一个 C++ 函数,为每个主题找到 measure1 的总和和 measure 2 的总和,然后根据这两个总和创建两个新的测量。最后,该函数应将这些新测量值作为列表返回。

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

// [[Rcpp::export]]
Rcpp::List mySlowListFn(Rcpp::List myList, int nSub)
   arma::vec myMult(nSub);
   arma::vec myDiv(nSub);
   for (int i = 0; i < nSub; i++)
     arma::vec meas1_i = Rcpp::as<arma::vec>(Rcpp::as<Rcpp::List>(myList["meas1"])[i]);
     arma::vec meas2_i = Rcpp::as<arma::vec>(Rcpp::as<Rcpp::List>(myList["meas2"])[i]);
     myMult[i] = arma::sum(meas1_i)*arma::sum(meas2_i);
     myDiv[i] = arma::sum(meas1_i)/arma::sum(meas2_i);
   
   return Rcpp::List::create(Rcpp::Named("myMult") = myMult, 
                             Rcpp::Named("myDiv") = myDiv);

我怎样才能使上面的功能更快?我特别在寻找将输入和输出列表保留在代码中的想法(因为在我自己的程序中处理列表是不可避免的),但有一些技巧可以减少一些开销时间。我想到的一件事是:

 Rcpp::List mySlowListFn(const Rcpp::List& myList, int nSub)

非常感谢您的帮助。

【问题讨论】:

你的问题结束于一个开放的猜想,你可以(我敢补充说,应该)测试。 您可以尝试一些技巧,例如使用糖函数或使用不复制的犰狳向量的构造函数。我不确定为什么列表会很慢。 @DirkEddelbuettel,您好 Dirk,我实际测试了它,我没有发现使用指针或使用 myList 本身之间有任何区别,这非常令人惊讶!考虑另一个调用 mySlowListFn above() 的函数。如果我们用指向 myList 的指针参数定义 mySlowListFn,我应该如何在 callerFn 中调用这个函数?我可以直接使用 use myList 作为参数还是我应该把它的地址,因为它假设是一个指针?我试图理解为什么使用指针技巧并没有让我的代码更快,我不确定我是否正确使用了这个技巧。非常感谢您的帮助。 没有区别因为我们通过SEXP类型与R接口已经是指针。添加const &amp; 只是C++ 级别的装饰。你射错了目标。 @Sameer,感谢 sameer 的评论。您特别指的是哪些糖功能?同样关于犰狳 vec 的想法,事实是我想在函数中保留列表,因为在我的原始代码中我必须使用 List 并且我想学习如何处理列表以制作我的原始函数兴趣更快。 【参考方案1】:

首先,请注意,列表的复制语义在最新版本的 R 中发生了变化(肯定是在最新的 R-devel 中,不确定它是否进入了 R 3.1.0),其中 浅拷贝列表被创建,并且如果它们被修改,稍后会复制其中的元素。如果您运行的是旧版本的 R,很有可能会阻碍其更昂贵的列表复制语义。

也就是说,这就是我将如何使用基准重写您的函数以提高速度的方法。 sourceCpp 在您自己的机器上进行比较。

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

// [[Rcpp::export]]
Rcpp::List mySlowListFn(Rcpp::List myList, int nSub)
   arma::vec myMult(nSub);
   arma::vec myDiv(nSub);
   for (int i = 0; i < nSub; i++)
     arma::vec meas1_i = Rcpp::as<arma::vec>(Rcpp::as<Rcpp::List>(myList["meas1"])[i]);
     arma::vec meas2_i = Rcpp::as<arma::vec>(Rcpp::as<Rcpp::List>(myList["meas2"])[i]);
     myMult[i] = arma::sum(meas1_i)*arma::sum(meas2_i);
     myDiv[i] = arma::sum(meas1_i)/arma::sum(meas2_i);
   
   return Rcpp::List::create(Rcpp::Named("myMult") = myMult, 
                             Rcpp::Named("myDiv") = myDiv);


// [[Rcpp::export]]
Rcpp::List myFasterListFn(Rcpp::List myList, int nSub) 

  Rcpp::NumericVector myMult = Rcpp::no_init(nSub);
  Rcpp::NumericVector myDiv = Rcpp::no_init(nSub);

  Rcpp::List meas1 = myList["meas1"];
  Rcpp::List meas2 = myList["meas2"];

  for (int i = 0; i < nSub; i++) 

    arma::vec meas1_i( 
      REAL(VECTOR_ELT(meas1, i)), Rf_length(VECTOR_ELT(meas1, i)), false, true
    );

    arma::vec meas2_i(
      REAL(VECTOR_ELT(meas2, i)), Rf_length(VECTOR_ELT(meas2, i)), false, true
    );

    myMult[i] = arma::sum(meas1_i) * arma::sum(meas2_i);
    myDiv[i] = arma::sum(meas1_i) / arma::sum(meas2_i);
  

  return Rcpp::List::create(
    Rcpp::Named("myMult") = myMult, 
    Rcpp::Named("myDiv") = myDiv
  );


/*** R
library(microbenchmark)
lappend <- function(lst, ...)
  lst <- c(lst, list(...))
  return(lst)


nSub <- 30
n <- 10
meas1 <- list()
meas2 <- list()
for (i in 1:nSub)
  meas1 <- lappend(meas1, rnorm(n))
  meas2 <- lappend(meas2, rnorm(n))

myList <- list(meas1 = meas1, meas2 = meas2)
x1 <- mySlowListFn(myList, nSub)
x2 <- myFasterListFn(myList, nSub)
microbenchmark(
  mySlowListFn(myList, nSub),
  myFasterListFn(myList, nSub)
)
*/

给我

> library(microbenchmark)

> lappend <- function(lst, ...)
+   lst <- c(lst, list(...))
+   return(lst)
+ 

> nSub <- 30

> n <- 10

> meas1 <- list()

> meas2 <- list()

> for (i in 1:nSub)
+   meas1 <- lappend(meas1, rnorm(n))
+   meas2 <- lappend(meas2, rnorm(n))
+ 

> myList <- list(meas1 = meas1, meas2 = meas2)

> x1 <- mySlowListFn(myList, nSub)

> x2 <- myFasterListFn(myList, nSub)

> microbenchmark(
+   mySlowListFn(myList, nSub),
+   myFasterListFn(myList, nSub)
+ )
Unit: microseconds
                         expr    min      lq  median      uq    max neval
   mySlowListFn(myList, nSub) 14.772 15.4570 16.0715 16.7520 42.628   100
 myFasterListFn(myList, nSub)  4.502  5.0675  5.2470  5.8515 18.561   100

RcppRcpp11 的未来版本将具有 ListOf&lt;T&gt; 类,这将使我们更容易与预先知道内部类型的列表进行交互,在正确的语义已经解决之后。

【讨论】:

非常好的答案,像往常一样。谢谢你,凯文。 如果你在这些列表中有一个矩阵而不是向量,你会怎么做?换句话说,你将如何修改矩阵( arma::vec meas2_i( REAL(VECTOR_ELT(meas2, i)), Rf_length(VECTOR_ELT(meas2, i)), false, true );)? R 矩阵只是具有维度的向量,因此您仍然可以使用 REAL 获取指针,然后使用例如获取行/列Rf_nrowsRf_ncols。或者,如果您尝试将矩阵放入 arma::vec,则无需更改任何内容(它将按“列”填充) 这实际上是我第一次看到 REAL 和 VECTOR_ELT。你会详细说明吗?我们还应该使用 VECTOR_ELT() 吗?老实说,我有两个挑战 1) 理解 arma::vec meas1_i( REAL(VECTOR_ELT(meas1, i)), Rf_length(VECTOR_ELT(meas1, i)), false, true );和 2)如何获得相似的矩阵 VECTOR_ELT 是来自基本 R API 的函数,它从 R 列表中提取元素 (SEXP)。由于SEXP 是一个不透明的指针,所以你得到的数据可能是向量、矩阵等。 REAL 是一个宏,它获取指向“数字”向量的“数字”存储数组的指针。我建议阅读R-ints 的前几章,因为我提出的解决方案有点超出 Rcpp。

以上是关于我的带有 Rcpp::List 输入的 C++ 函数非常慢的主要内容,如果未能解决你的问题,请参考以下文章

C++:你在使用 Loki 还是 Boost 作为函子?

Rcpp:通过引用列出<->矩阵转换?? + 使用矩阵编程时优化内存分配

C++ 成员函数指针和 STL 算法

SWIG 输入文件和带有 numpy 的向量。使用 % 应用?

带有输入无符号字符缓冲区 C++ 的 DeviceIoControl

将带有内部空字符的python2.7字符串传递给c ++