117

学习如何编写用于 R 的 C 代码的最佳资源是什么?我知道 R 扩展的系统和外语接口部分,但我觉得很难。用于编写与 R 一起使用的 C 代码的好资源(在线和离线)是什么?

澄清一下,我不想学习如何编写 C 代码,我想学习如何更好地集成 R 和 C。例如,如何从 C 整数向量转换为 R 整数向量(反之亦然)还是从 C 标量到 R 向量?

4

4 回答 4

73

好吧,有个好老使用来源,卢克!--- R 本身有很多(非常有效的)C 代码可供学习,CRAN 有数百个包,其中一些来自您信任的作者。这提供了真实的、经过测试的例子来学习和适应。

但正如 Josh 所怀疑的那样,我更倾向于 C++,因此更倾向于Rcpp。它也有很多例子。

编辑:有两本书我觉得很有帮助:

  • 第一个是 Venables 和 Ripley 的“ S Programming ”,尽管它已经很长了(并且多年来一直有传言说第二版)。当时根本没有别的。
  • Chambers 的“数据分析软件”中的第二个是更新的,并且具有更好的以 R 为中心的感觉——以及关于扩展 R 的两章。C 和 C++ 都被提及。另外,约翰因为我对摘要所做的事情而撕碎了我,所以仅此一项就值得入场。

也就是说,John 越来越喜欢Rcpp(并做出贡献),因为他发现 R 对象和 C++ 对象(通过Rcpp)之间的匹配非常自然——而 ReferenceClasses 对此有所帮助。

编辑 2: 对于 Hadley 重新聚焦的问题,我强烈建议您考虑 C++。与 C 相关的样板废话太多了——非常乏味且非常可避免。看看Rcpp-introduction vignette。另一个简单的例子是这篇博客文章,我展示了不用担心 10% 的差异(在 Radford Neal 的一个例子中),我们可以使用 C++ 获得80 倍的增长(当然这是一个人为的例子)。

编辑 3:您可能会遇到 C++ 错误,这很复杂,说得委婉些,很难理解。但是仅仅使用 Rcpp而不是扩展它,你几乎不需要它。虽然这个成本是不可否认的,但它的好处远远超过了更简单的代码、更少的样板、没有保护/取消保护、没有内存管理等 pp。Doug Bates 昨天表示他发现 C++ 和 Rcpp 更像是编写 R比写 C++。YMMV 等等。

于 2010-11-05T13:36:54.860 回答
58

哈德利,

您绝对可以编写类似于 C 代码的 C++ 代码。

我理解你所说的 C++ 比 C 更复杂的说法。如果你想掌握一切:对象、模板、STL、模板元编程等......大多数人不需要这些东西,只能依赖其他东西给它。Rcpp的实现很复杂,但是你不知道你的冰箱是怎么工作的,并不代表你不能开门拿鲜奶……

从您对 R 的许多贡献中,让我印象深刻的是您发现 R 有点乏味(数据操作、图形、字符串操作等......)。用 R 的内部 C API 为更多惊喜做好准备。这非常乏味。

我不时阅读 R-exts 或 R-ints 手册。这有帮助。但大多数时候,当我真的想了解一些东西时,我会进入 R 源代码,以及例如 Simon 编写的包的源代码(那里通常有很多东西要学)。

Rcpp 旨在消除 API 的这些繁琐方面。

您可以根据一些示例自行判断您发现更复杂、更模糊等的内容。此函数使用 C API 创建一个字符向量:

SEXP foobar(){
  SEXP ab;
  PROTECT(ab = allocVector(STRSXP, 2));
  SET_STRING_ELT( ab, 0, mkChar("foo") );
  SET_STRING_ELT( ab, 1, mkChar("bar") );
  UNPROTECT(1);
}

使用 Rcpp,您可以编写与以下相同的函数:

SEXP foobar(){
   return Rcpp::CharacterVector::create( "foo", "bar" ) ;
}

或者:

SEXP foobar(){
   Rcpp::CharacterVector res(2) ;
   res[0] = "foo" ;
   res[1] = "bar" ;
   return res ;
}

正如德克所说,在几个小插曲上还有其他例子。我们通常还会将人们引向我们的单元测试,因为他们每个人都测试代码的一个非常具体的部分,并且在某种程度上是不言自明的。

我在这里显然有偏见,但我建议熟悉 Rcpp 而不是学习 R 的 C API,然后如果有不清楚的地方或 Rcpp 似乎不可行,请访问邮件列表。

无论如何,销售宣传结束。

我想这一切都取决于您最终要编写什么样的代码。

罗曼

于 2010-11-05T14:47:20.160 回答
30

@hadley:不幸的是,我没有具体的资源来帮助您开始使用 C++。我是从 Scott Meyers 的书(Effective C++、更有效的 C++ 等)中挑选出来的,但这些并不是真正可以称之为介绍性的。

我们几乎只使用 .Call 接口来调用 C++ 代码。规则很简单:

  • C++ 函数必须返回一个 R 对象。所有 R 对象都是 SEXP。
  • C++ 函数将 0 到 65 个 R 对象作为输入(同样是 SEXP)
  • 它必须(不是真的,但我们可以保存它以供以后使用)使用 C 链接声明,可以使用extern "C"或 Rcpp 定义的RcppExport别名。

所以一个 .Call 函数在一些头文件中被这样声明:

#include <Rcpp.h>

RcppExport SEXP foo( SEXP x1, SEXP x2 ) ;

并在 .cpp 文件中像这样实现:

SEXP foo( SEXP x1, SEXP x2 ){
   ...
}

关于使用 Rcpp 的 R API 没有更多的了解。

大多数人只想处理 Rcpp 中的数字向量。您可以使用 NumericVector 类执行此操作。有几种方法可以创建数值向量:

从您从 R 传递下来的现有对象:

 SEXP foo( SEXP x_) {
    Rcpp::NumericVector x( x_ ) ;
    ...
 }

使用 ::create 静态函数给定值:

 Rcpp::NumericVector x = Rcpp::NumericVector::create( 1.0, 2.0, 3.0 ) ;
 Rcpp::NumericVector x = Rcpp::NumericVector::create( 
    _["a"] = 1.0, 
    _["b"] = 2.0, 
    _["c"] = 3
 ) ;

给定尺寸:

 Rcpp::NumericVector x( 10 ) ;      // filled with 0.0
 Rcpp::NumericVector x( 10, 2.0 ) ; // filled with 2.0

然后一旦你有了一个向量,最有用的就是从中提取一个元素。这是通过操作符 [] 完成的,具有基于 0 的索引,因此例如对数字向量的值求和如下所示:

SEXP sum( SEXP x_ ){
   Rcpp::NumericVector x(x_) ;
   double res = 0.0 ;
   for( int i=0; i<x.size(), i++){
      res += x[i] ;
   }
   return Rcpp::wrap( res ) ;
}

但是使用 Rcpp 糖,我们现在可以做得更好:

using namespace Rcpp ;
SEXP sum( SEXP x_ ){
   NumericVector x(x_) ;
   double res = sum( x ) ;
   return wrap( res ) ;
}

正如我之前所说,这完全取决于您要编写什么样的代码。查看人们在依赖 Rcpp 的软件包中做了什么,检查小插图,单元测试,在邮件列表中返回给我们。我们总是很乐意提供帮助。

于 2010-11-08T12:32:46.650 回答
20

@jbremnant:没错。Rcpp 类实现了一些接近 RAII 模式的东西。创建 Rcpp 对象时,构造函数会采取适当的措施来确保底层 R 对象 (SEXP) 免受垃圾收集器的影响。析构函数撤销保护。这在Rcpp-intrduction小插图中进行了解释。底层实现依赖于 R API 函数R_PreserveObjectR_ReleaseObject

由于 C++ 封装,确实存在性能损失。我们尝试通过内联等将其保持在最低限度……代价很小,而且当您考虑到编写和维护代码所需的时间方面的收益时,它就没有那么重要了。

从 Rcpp 类 Function 调用 R 函数比使用 C api 直接调用 eval 慢。这是因为我们采取了预防措施并将函数调用包装到 tryCatch 块中,以便我们捕获 R 错误并将它们提升为 C++ 异常,以便可以使用 C++ 中的标准 try/catch 处理它们。

大多数人都想使用向量(特别是 NumericVector),而且这个类的惩罚非常小。examples/ConvolveBenchmarks 目录包含来自 R-exts 的臭名昭著的卷积函数的几个变体,并且小插图具有基准测试结果。事实证明,Rcpp 比使用 R API 的基准代码更快。

于 2010-11-08T12:12:18.190 回答