跨越R与C++的桥梁:Rcpp
因为写R扩展的需要,我用Rcpp其实有一阵子了。我一直在强调,把方便和丰富的R环境与系统级语言如C/C++等结合才是适合我现时计算的王道。但R自带的C/C++的API接口实在是太难用,很是限制人的使用欲望。我试图把它们用最简单的方式叙述过,比如可以看这里和这里。但实际应用中无法因繁就简,还是避免不了写一大堆重复代码,记一大堆生僻的API。于是Rcpp成了我的救星,特别是前两天升级后,我意外地发现了很多新的特性,虽然还没真正使用,但估计会使得我的工作更有效率。把这个包的几篇文档拿来研究了一番,做了些笔记。
Rcpp的终极目标,是通过C++面向对象与模板编程的机制,把几乎所有R API与数据对象都封装成类以及类的方法。这样,写C++扩展的人只需要了解这些类以及这些类的调用即可,而把接口定义、垃圾回收(gc)、异常捕获、数据类型的转换等等对于R/C++交互都是必须的工作都隐藏了起来。新版的Rcpp在效率上更是花了大功夫,一个突出的特点是减少了数据对象的拷贝转换,直接对原来的R对象进行操作,这样无论是时间上还是空间上,都获得了很大的好处(这些额外的耗费我在使用旧版Rcpp时深有体会,新版我还没深入使用,不知道会提升多少)。这里的论述都是针对新版(目前是0.9.1)Rcpp的,相比而言,旧版的接口真是浮云。
一个例子: Rcpp与基本的R API在写扩展时的对比
见下图的两段代码:
(a)里的几个你不熟悉的数据类型与函数都是R API定义的类型与函数,其中包括了R对象的创建、数据类型的转换、内存对象的保护(预防gc),看起来很别扭,光这些API不知又得耗费你多少的脑容量。(b)就很简单了,整个就一正常的c++代码,只是引入了一个Rcpp名空间下的NumericVector类而已。事实上,Rcpp利用类的继承机制,为几乎所有的R基本数据类型都建立了一个类来管理,比如这里的NumericVector类就是管理数值型向量数据类型的。
Rcpp的类层次结构
Rcpp管理R对象的基类叫RObject,正如前面所述,可以传递一个R对象以构造这个类的实例,旧版的处理会把对象拷贝成一个C++的STL对象,新版则只是简单地保存了一个对它的引用,类仅充当一个代理的角色,把所有的操作都在内部通过R API来完成。RObject还提供了一些对象通用的方法,如对象属性查询方法:isNULL、isObject、isS4;对象属性管理方法:attributeNames, hasAttribute, attr;slots的管理方法:hasSlot, slot。这些方法对于R用户来说应该都很熟悉,就不用去查那晦涩难懂的R API函数了。由这个基类派生出来的子类则负责具体对象的特定的处理,如Vector、Matrix、Character、Environment、Function等对象的子类。不同的数据对象可能有不同的数据类型,所以又有不同的类来处理,比如Vector就有IntegerVector, NumericVector, RawVector, LogicalVector, CharacterVector, GenericVector(List), ExpressionVector。
R与C++间的数据类型转换
虽说Rcpp只保存R对象的一个引用,但两种不同数据格式间的转换肯定也是必须的,所以有了Rcpp::wrap与Rcpp::as函数,前者用于把C++对象转换成R对象,后者则相反。这两个函数都是用C++的模板元编程技术实现的。这些转换有时候是隐式调用的,如赋值时两边的类型不匹配,就会作出自动转换。
异常捕获
由于R与C++使用不同的异常捕获模型,所以要加以处理,才能在R环境中捕获到来自C++的异常消息。所以,Rcpp提供了BEGIN_RCPP与END_RCPP这一对宏,只要把有可能出现C++异常的代码放到这对宏里,就可以在R环境中接收到C++的异常。
性能比较
Rcpp重新设计的一个重要思路就是使得对象的拷贝尽量地少,同时其设计也借鉴了STL的设计原理,比如对iterator的使用。虽然作者对取值operator[]作了多方的优化,但性能却是不如iterator好。下面的表格是对一个卷积计算多次的计算时间比较。
R API是原生的支持,这里用作参照。可以看到的是旧版的Rcpp实现(最后一行)的性能比原生支持差得甚远,这是因为有过多的对象拷贝与转换的缘故。使用了operator[]的实现性能有了很大的提升,但还是比不上原生的支持。但使用了 iterator实现的版本,已经跟原生支持的效率差不多了。更神奇的是,第二行名为Rcpp sugar的实现比原生支持更快。这是一个仍在开发完善中的模块,但目前已经拥有很多的功能了。这个模块的目的在于在C++里也能充分地利用R的向量化计算的特性,不但代码写起来感觉跟R的思路很类似,效率上也得到了很大的提升。
正在进行中的开发
上面已经提到,Rcpp sugar是一个正在开发完善中的模块,拥有它之后,你可以在C++层面调用更多的R的函数,使用更多的R的特性。另一个重要的模块是Rcpp modules,它启发自Boost.Python模块,通过这个模块,要把C++函数导入到R变得更为简单,只要通过Rcpp的宏RCPP_MODULE把一般的C++函数包装一下,就可以直接在R环境中导入并使用,完全不需要额外的转换工作。
Rcpp的作者还在开发一个名为RcppArmadillo的包,Armadillo是一个模板化的C++线性代数包,试图在效率与易用性上寻求个平衡点,而RcppArmadillo就是一个接口,使得在R中也能使用Armadillo。