9

Rcpp 简明入门

 3 years ago
source link: https://cosx.org/2013/12/rcpp-introduction/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Rcpp 牛到什么程度,我想不用我多说。光是看 Author 五人组的名字就足够唬人了(简直是 R 包开发男子天团了)。最近正在为实验室开发 R 包(平生第一次),愉快地用起了 Rcpp。有过这些天之后觉得感觉真的很好,于是也就厚颜无耻地写篇小文介绍一下。以下的内容都是在 Linux 下完成了,Windows 的朋友们需要安装 Rtools 才能用上 Rcpp。

1. cppFunction 和 sourceCpp

首先我们来看看第一个例子。

cppFunction(
    'int fib_cpp_0(int n){
         if(n==1||n==2) return 1;
         return(fib_cpp_0(n-1)+fib_cpp_0(n-2));
    }'
)

这个例子是老掉牙的计算第 n 个 Fibonacci 数的函数,不过既然 Dirk 老爷子也用这个例子,我们也就将就一下把。运行这段代码之后,你可以在 R 的 Workplace 中看到一个名为 fib_cpp_0 的函数。我们来看看跟 R 版本的 Fib_r 的对比。

fib_r <- function(n){
    if(n==1||n==2) return(1)
    return(fib_r(n-1)+fib_r(n-2))
}

这两个函数运行的时间对比如下:

> system.time(fib_r(30))
   user  system elapsed
  3.080   0.000   3.083
> system.time(fib_cpp_0(30))
   user  system elapsed
  0.004   0.000   0.004

在这个对比中,我们可以发现,fib_cpp_0fib_r快得多了。虽然如此,我觉得这个比较没有什么意思,原因在于这个对比中没有体现 R 向量化运算的优势。不过考虑到相当多的人在写 R 的代码时根本不考虑向量化(我忏悔,我就是其中之一),Rcpp 还真的能解决很多效率问题。

我并不喜欢cppFunction这种 C++ 代码和 R 代码混合在一起的风格。Rcpp 提供了另外一种方法让我们简单调用 C++ 代码: sourceCpp(有没有想起 R 里面的 source?)

#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
int fib_cpp_1(int n)
{
    if(n==1||n==2) return 1;
    return fib_cpp_1(n-1)+fib_cpp_1(n-2);
}

在 R 中只要调用

sourceCpp("fib_cpp_1.cpp")

于是我们又得到了一个fib_cpp_1函数。它跟fib_cpp_0的对比如下:

> system.time(fib_cpp_0(50))
用户    系统    流逝 
194.077   0.153 195.575 
> system.time(fib_cpp_1(50))
用户    系统    流逝 
152.336   0.221 157.190

在上面可以看出用sourceCpp生成的函数fib_cpp_1在计算速度上比用cppFunction生成的fib_cpp_0更快。至于原因是什么,我还没有发现,一旦发现,我很乐意再次写出来跟大家分享。

其实,cppFunctionsourceCpp的本质是什么?

我们来回忆一下在没有 Rcpp 之前我们是如何调用 C/C++ 的。在那个年代,我们会先写出 C/C++ 的代码,然后用R CMD SHLIB生成一个动态链接库,然后再用dyn.load载入这个动态链接库。最后用.Call(或者. C,当然这个太老了),调用库中的函数。

然后我们直接输入fib_cpp_1看看它们的庐山真面目:

function (n)
.Primitive(".Call")(, n)

从上面我们可以看到,其实通过cppFunctionsourceCpp得到的这个函数,本质上还是用.Call调用动态链接库中编译好的 C++ 函数。只不过 Rcpp 帮你把一些麻烦的步骤省略下来了。

2. 编译动态链接库

Rcpp 自带了一个函数SHLIB这个函数可以帮你将使用了 Rcpp.h 的 cpp 文件编译成动态链接库。这个函数源代码在 Rcpp 包中 SHLIB.R 下面。你可以在 R 的控制台中调用

Rcpp:::SHLIB("test.cpp")

来编译你的动态链接库。之所以要加前面这一串,是因为这个函数没有在 Rcpp 包的 namespace 中 export。当然你在 shell 下面可以用

Rscript -e "Rcpp:::SHLIB('test.cpp')"

来编译(这个技巧来自 (http://thirdwing.github.io/2013/10/25/rcpp/ ).

如果我们进去看看这个函数的源代码,我们会发现,其实它做的事情就是为编译器指明库的位置(通过添加环境变量)。

如果要用这种方法调用 cpp 程序的话,cpp 的写法会有一些规定,当然这跟平时我们直接用 R API 调用 C 语言的写法很相似。所以如果大家只是想用 C/C++ 来解决性能瓶颈的话,我建议大家还是使用sourceCpp或者cppFunction好了。

3. 用 Rcpp 开发 R package

一如 R 自家踢狗的package.skeletion一样,Rcpp 提供了一个函数Rcpp.package.skeletion,运行这个函数:

Rcpp.package.skeletion("RcppDemo")

然后你就可以在当前工作目录下找到一个名为 RcppDemo 的目录,目录的结构如下:

Demo
|-- DESCRIPTION
|-- man
|   |-- Demo-package.Rd
|   `-- rcpp_hello_world.Rd
|-- NAMESPACE
|-- R
|   `-- RcppExports.R
|-- Read-and-delete-me
`-- src
    |-- Makevars
    |-- Makevars.win
    |-- RcppExports.cpp
    `-- rcpp_hello_world.cpp

不过说回来,每个 R package 都差不多的啦。这个几乎是空的 package 里面已经有一个写好的 Hello world 函数。当然我们这里就不拿这个经典函数来作例子了。

#include <Rcpp.h>
using namespace Rcpp;

RcppExport SEXP dftest(SEXP df, SEXP vname)
{
    DataFrame DF = as<DataFrame>(df);
    std::string var = as<std::string>(vname);
    NumericVector v = DF[var];
    return(wrap(v));
}

在 R 文件夹下面,我们再定义如下函数:

DFtest <- function(dframe, varname){
    vcol <- .Call("dftest", dframe, varname)
    return(vcol)
}

这个函数的作用是返回一个 data.frame 中的一列,这一列的名字由用户提供。假设你把这个 Demo 打包了,然后安装载入,你就可以调用DFtest。假设你调用DFtest(cars, "speed"),那么你会得到cars$speed

这个例子其实也没有太多的技术含量。不过还是有一点意思的。我们可以先看看其中的源代码。

第一行指明需要用到 Rcpp.h。第二行指明命名空间是 Rcpp。

第三行中的 RcppExport 的作用是,当你把这个 cpp 文件编译成动态链接库之后,RcppExport 后面的函数可以被. Call 调用。而实际上这个 RcppExport 是一个宏。根据 “[Rcpp-devel] What are RcppExport, BEGINRCPP and ENDRCPP?” 的说法:

#define RcppExport extern "C"

第三行中,我们传入了两个SEXP作为参数。这两个参数在第四行和第五行中用函数Rcpp::as分别转换为DataFramestd::string。第六行中我们取出这个数据框中名为 var 的那一列,赋值给一个NumericVector.

在最后一行Rcpp::wrap将这个NumericVector重新转换成一个SEXP,然后传回 R 中。

4. 总的来说

因为 Rcpp,我们可以在 C++ 中操作一些我们熟悉的 R 对象,而且更妙的是,分配内存这种粗重功夫不用我们自己撸袖子上了,实在可喜可贺。当然现在还有一些问题,比方说,现在我们在 C++ 中操作 DataFrame 的时候,还不能像在 R 中随意切割那么潇洒,再比如,虽然 Rcpp 中的 sugar 可以让我们直接将一个NumericVector与一个数值比较,然后得到一个LogicalVector,但我们无法用类似 R 中A[B>0]这样的语句去操作 C++ 中的NumericVector或者 DataFrame(或者已经有了而我不知道)。

虽然如此,但是 Rcpp 的前景还是一片光明的,利用 Rcpp 来开发 R package 是一件愉快的事情。我们莫忘了 Rcpp 开发出来的目的,不在其速度,而在于它让我们能更好地进行 R 的开发。

严肃科技平台开发工程师,几乎是纯码农一枚。 参与翻译《R 绘图系统》、《Rcpp:R 与 C++ 的无缝整合》。张晔

敬告各位友媒,如需转载,请与统计之都小编联系(直接留言或发至邮箱:[email protected]),获准转载的请在显著位置注明作者和出处(转载自:统计之都),并在文章结尾处附上统计之都微信二维码。

统计之都微信二维码

← 国债收益率的影响因素 纪念贝叶斯定理 250 周年 暨首届中国贝叶斯统计学术论坛(天津) →

发表 / 查看评论


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK