Rcpp函数比Rf_eval慢

3
我一直在开发一个使用Rcpp对大型医学图像文件组应用任意R代码的软件包。我注意到,我的Rcpp实现比原始的纯C版本慢得多。我发现区别在于通过Function调用函数,而不是原始的Rf_eval。我的问题是为什么性能降低了接近4倍,是否有方法可以加快函数调用以使其性能更接近于Rf_eval?
library(Rcpp)                                                                                                                                                          
library(inline)                                                                                                                                                        
library(microbenchmark)                                                                                                                                                

cpp_fun1 <-                                                                                                                                                            
  '                                                                                                                                                                    
Rcpp::List lots_of_calls(Function fun, NumericVector vec){                                                                                                             
  Rcpp::List output(1000);                                                                                                                                             
  for(int i = 0; i < 1000; ++i){                                                                                                                                       
    output[i] = fun(NumericVector(vec));                                                                                                                               
  }                                                                                                                                                                    
  return output;                                                                                                                                                       
}                                                                                                                                                                      
'                                                                                                                                                                      

cpp_fun2 <-                                                                                                                                                            
  '                                                                                                                                                                    
Rcpp::List lots_of_calls2(SEXP fun, SEXP env){                                                                                                                         
  Rcpp::List output(1000);                                                                                                                                             
  for(int i = 0; i < 1000; ++i){                                                                                                                                       
    output[i] = Rf_eval(fun, env);                                                                                                                                     
  }                                                                                                                                                                    
  return output;                                                                                                                                                       
}                                                                                                                                                                      
'                                                                                                                                                                      

lots_of_calls <- cppFunction(cpp_fun1)                                                                                                                                 
lots_of_calls2 <- cppFunction(cpp_fun2)                                                                                                                                

microbenchmark(lots_of_calls(mean, 1:1000),                                                                                                                            
               lots_of_calls2(quote(mean(1:1000)), .GlobalEnv))

结果

Unit: milliseconds
                                            expr      min       lq     mean   median       uq      max neval
                     lots_of_calls(mean, 1:1000) 38.23032 38.80177 40.84901 39.29197 41.62786 54.07380   100
 lots_of_calls2(quote(mean(1:1000)), .GlobalEnv) 10.53133 10.71938 11.08735 10.83436 11.03759 18.08466   100

2
你是否意识到当你使用 Rcpp::Function 从 C++ 调用 R 函数时,会带来相应的开销?你怎么能期望这比直接在 R 中运行更快呢? - Dirk Eddelbuettel
1
此外,如果您认为Rf_eval()适合您的需求,为什么不使用它呢?正如示例所示,Rcpp并不会阻止您这样做。 - Dirk Eddelbuettel
2
此外,在C++上下文中直接调用Rf_eval()是危险的,因为R错误(即C longjmps)将绕过C ++对象的析构函数,从而导致内存泄漏/通常会引起未定义行为。Rcpp::Function试图确保不会发生这种情况。 - Kevin Ushey
@DirkEddelbuettel 我没想到从c++调用R函数会比R更快,但我也没想到会慢4倍。Rf_eval有点用,但这是个hack。为了在文件上实现类似apply的功能,用户传递了一个带单变量的引用函数调用,并从文件中提取了值并将其install到全局环境中。然后使用引用调用eval和全局环境。显然,我想要一种更清晰/更安全的方法,只是想减少性能损失。 - C. Hammill
你可以随意构建本地、特定的、有些 hack-ish 的解决方案。只要明白我们提供的是一个更通用的解决方案,具有不同的目标,如健壮性。从这个意义上说,你的个人思考关于 Rcpp 已经超出了范围。话虽如此,我想邀请你参与到 PR 中,削减你可能发现的 Rcpp::Function 中的任何松弛或多余部分。虽然我怀疑这里没有太多。 - Dirk Eddelbuettel
我真诚地建议选择预先构建的函数,这些函数由映射调用或使用共享库链接方法,以便用户可以提供自己的cpp,如果使用r来处理数据不是一个选项。您可能希望在代码审查中发布问题示例。 - coatless
2个回答

4

Rcpp 很棒,因为它使得程序员的代码看起来异常简洁。这种简洁性以模板响应和一系列假设为代价,从而拖慢了执行时间。但是,这就是通用代码与特定代码设置之间的情况。

举个例子,以 Rcpp::Function 的调用路线为例。初始构建和对 Rf_reval 的修改版本的外部调用需要一个特殊的 Rcpp 特定评估函数,该函数在 Rcpp_eval.h 中给出。反过来,此函数被包装在保护中,以防止通过与其关联的 Shield 调用 R 时出现函数错误。等等...
相比之下,Rf_eval两者都没有。如果它失败了,你就会一筹莫展。(当然,如果你通过R_tryEval实现错误捕获,情况就不同了。)
话虽如此,加速计算的最佳方法是在C++中编写计算所需的所有内容。

3
我喜欢让代码看起来异常干净,也许有一天我会引用这句话 :) - Dirk Eddelbuettel
@DirkEddelbuettel 尽管去做吧! - coatless
1
感谢@Coatless,我觉得错误捕获会让执行时间增加四倍,这令我感到惊讶。这个函数启发了我提出这个问题,它的目的是让用户对一组文件应用任意的R代码,有点像bigmemory。据我所知,完全将其转换为C++是不可能的。因此,我认为根本问题仍然是如何以最高效的方式使用Rcpp和/或R-C API实现通用的apply函数。 - C. Hammill
你可以从 R 端轻松地对数据进行任意 R 函数的扫描。 - Dirk Eddelbuettel

2
除了@coatless提到的观点外,你甚至没有在比较同类。你的示例没有将向量传递到函数中,并且更重要的是,通过quote()对函数进行了操作。
简而言之,这有点儿傻。
下面是一个更完整的示例,使用糖函数mean()
#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
List callFun(Function fun, NumericVector vec) {
  List output(1000);
  for(int i = 0; i < 1000; ++i){
    output[i] = fun(NumericVector(vec));
  }
  return output;
}

// [[Rcpp::export]]
List callRfEval(SEXP fun, SEXP env){
  List output(1000);
  for(int i = 0; i < 1000; ++i){
    output[i] = Rf_eval(fun, env);
  }
  return output;
}

// [[Rcpp::export]]
List callSugar(NumericVector vec) {
  List output(1000);
  for(int i = 0; i < 1000; ++i){
    double d = mean(vec);
    output[i] = d;
  }
  return output;
}

/*** R
library(microbenchmark)
microbenchmark(callFun(mean, 1:1000),   
               callRfEval(quote(mean(1:1000)), .GlobalEnv),
               callSugar(1:1000))
*/

您可以直接使用sourceCpp()函数:
R> sourceCpp("/tmp/ch.cpp")

R> library(microbenchmark)

R> microbenchmark(callFun(mean, 1:1000), 
+                callRfEval(quote(mean(1:1000)), .GlobalEnv),
+                callSugar(1:1000))
Unit: milliseconds
                                        expr      min       lq     mean   median       uq       max neval
                       callFun(mean, 1:1000) 14.87451 15.54385 18.57635 17.78990 18.29127 114.77153   100
 callRfEval(quote(mean(1:1000)), .GlobalEnv)  3.35954  3.57554  3.97380  3.75122  4.16450   6.29339   100
                           callSugar(1:1000)  1.50061  1.50827  1.62204  1.51518  1.76683   1.84513   100
R> 

谢谢Dirk,如另一条评论所述,sugar不适合我的需求,我正在尝试实现一个通用的文件应用。我也不太确定我是否理解了引用表达式对函数的影响,以及它是如何绕过传递函数和参数的。Rf_eval不会评估表达式吗? - C. Hammill
让我们就在这里对范围和目标达成不同意见,互相尊重各自的观点。 - Dirk Eddelbuettel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接