我的带有 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 &
只是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
Rcpp
和 Rcpp11
的未来版本将具有 ListOf<T>
类,这将使我们更容易与预先知道内部类型的列表进行交互,在正确的语义已经解决之后。
【讨论】:
非常好的答案,像往常一样。谢谢你,凯文。 如果你在这些列表中有一个矩阵而不是向量,你会怎么做?换句话说,你将如何修改矩阵( arma::vec meas2_i( REAL(VECTOR_ELT(meas2, i)), Rf_length(VECTOR_ELT(meas2, i)), false, true );)? R 矩阵只是具有维度的向量,因此您仍然可以使用REAL
获取指针,然后使用例如获取行/列Rf_nrows
,Rf_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++ 函数非常慢的主要内容,如果未能解决你的问题,请参考以下文章
Rcpp:通过引用列出<->矩阵转换?? + 使用矩阵编程时优化内存分配
SWIG 输入文件和带有 numpy 的向量。使用 % 应用?