Rcpp简明入门
2016-01-19 17:38
375 查看
2013/12/07统计软件C/C++、cppFunction、Rcpp、R包开发、sourceCppCOS编辑部
本文作者:张晔,中山大学数学与计算科学学院硕士在读,研究方向为生物统计,现就职于华南统计科学中心。
Rcpp牛到什么程度,我想不用我多说。光是看Author五人组的名字就足够唬人了(简直是R包开发男子天团了)。最近正在为实验室开发R包(平生第一次),愉快地用起了Rcpp。有过这些天之后觉得感觉真的很好,于是也就厚颜无耻地写篇小文介绍一下。以下的内容都是在Linux下完成了,Windows的朋友们需要安装Rtools才能用上Rcpp。
首先我们来看看第一个例子。
这个例子是老掉牙的计算第n个Fibonacci数的函数,不过既然Dirk老爷子也用这个例子,我们也就将就一下把。运行这段代码之后,你可以在R的Workplace中看到一个名为fib_cpp_0的函数。我们来看看跟R版本的Fib_r的对比。
这两个函数运行的时间对比如下:
在这个对比中,我们可以发现,
我并不喜欢
在R中只要调用
于是我们又得到了一个
在上面可以看出用
其实,
我们来回忆一下在没有Rcpp之前我们是如何调用C/C++的。在那个年代,我们会先写出C/C++的代码,然后用
然后我们直接输入
从上面我们可以看到,其实通过
Rcpp自带了一个函数
来编译你的动态链接库。之所以要加前面这一串,是因为这个函数没有在Rcpp包的namespace中export。当然你在shell下面可以用
来编译(这个技巧来自(http://thirdwing.github.io/2013/10/25/rcpp/ ).
如果我们进去看看这个函数的源代码,我们会发现,其实它做的事情就是为编译器指明库的位置(通过添加环境变量)。
如果要用这种方法调用cpp程序的话,cpp的写法会有一些规定,当然这跟平时我们直接用R API调用C语言的写法很相似。所以如果大家只是想用C/C++来解决性能瓶颈的话,我建议大家还是使用
一如R自家踢狗的
然后你就可以在当前工作目录下找到一个名为RcppDemo的目录,目录的结构如下:
不过说回来,每个R package都差不多的啦。这个几乎是空的package里面已经有一个写好的Hello world函数。当然我们这里就不拿这个经典函数来作例子了。
在R文件夹下面,我们再定义如下函数:
这个函数的作用是返回一个data.frame中的一列,这一列的名字由用户提供。假设你把这个Demo打包了,然后安装载入,你就可以调用
这个例子其实也没有太多的技术含量。不过还是有一点意思的。我们可以先看看其中的源代码。
第一行指明需要用到Rcpp.h。第二行指明命名空间是Rcpp。
第三行中的RcppExport的作用是,当你把这个cpp文件编译成动态链接库之后,RcppExport后面的函数可以被.Call调用。而实际上这个RcppExport是一个宏。根据"[Rcpp-devel]
What are RcppExport, BEGIN_RCPP and END_RCPP?" 的说法:
第三行中,我们传入了两个
在最后一行
因为Rcpp,我们可以在C++中操作一些我们熟悉的R对象,而且更妙的是,分配内存这种粗重功夫不用我们自己撸袖子上了,实在可喜可贺。当然现在还有一些问题,比方说,现在我们在C++中操作DataFrame的时候,还不能像在R中随意切割那么潇洒,再比如,虽然Rcpp中的sugar可以让我们直接将一个
虽然如此,但是Rcpp的前景还是一片光明的,利用Rcpp来开发R package是一件愉快的事情。我们莫忘了Rcpp开发出来的目的,不在其速度,而在于它让我们能更好地进行R的开发。
本文作者:张晔,中山大学数学与计算科学学院硕士在读,研究方向为生物统计,现就职于华南统计科学中心。
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_0比
fib_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更快。至于原因是什么,我还没有发现,一旦发现,我很乐意再次写出来跟大家分享。
其实,
cppFunction和
sourceCpp的本质是什么?
我们来回忆一下在没有Rcpp之前我们是如何调用C/C++的。在那个年代,我们会先写出C/C++的代码,然后用
R CMD SHLIB生成一个动态链接库,然后再用
dyn.load载入这个动态链接库。最后用
.Call(或者.C,当然这个太老了),调用库中的函数。
然后我们直接输入
fib_cpp_1看看它们的庐山真面目:
function (n) .Primitive(".Call")(, n)
从上面我们可以看到,其实通过
cppFunction和
sourceCpp得到的这个函数,本质上还是用
.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, BEGIN_RCPP and END_RCPP?" 的说法:
#define RcppExport extern "C"
第三行中,我们传入了两个
SEXP作为参数。这两个参数在第四行和第五行中用函数
Rcpp::as分别转换为
DataFrame和
std::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的开发。
相关文章推荐
- 详解C++编程中一元运算符的重载
- C++设计模式——享元模式
- C++中#define、typedef和const的区别
- 详解在C++中显式默认设置的函数和已删除的函数的方法
- 类的组合和继承
- C++经典面试题
- Java内存模型FAQ(二) 其他语言,像C++,也有内存模型吗?
- 改善程序与设计的55个具体做法之一:视C++为语言联邦
- operator new在C++中的各种写法
- 1008. Elevator (20)
- C语言中extern的用法
- 微机接口课设 汽车控制系统 C语言源代码 Proteus 8086
- 1006. Sign In and Sign Out (25)
- 1005. Spell It Right (20)
- C++使用cout补0
- 困扰很久的汉诺塔问题c语言实现
- vector VS list in C++
- 《C/C++专项练习》 — (3)
- C++之路进阶——splay树(宠物收养所)
- leetcode Move Zeros