3 min read

Rcpp读书笔记2-第二章工具与设置

2.1 总体设置

Rcpp提供一个C++应用编程接口(API),来扩展R系统。运行Rcpp和R的一些需求:

  • 开发环境需要一个C++编译器。
  • 通过动态链接和嵌入方式,供R调用。
  • 在windows下,一般会通过安装Rtools来提供开发环境

编译R包所需要的标准环境,通常也是编译Rcpp所需要的。
几个附加的R包非常有用:

  • inline 对于一些短的C++代码,可以直接通过inline内部编译和调用。
  • rbenchmark 用来对比测试不同代码的性能。microbenchmark包也是一个非常好的选择。
  • RUnit 用作单元测试。

2.2 编译器

windows下,通常使用Rtools自带的g++编译器。

2.3 R API

R自身提供了API。API在“Writting R Extensions”手册中进行了详细描述,定义了R安装所需要的头文件。R核心开发团队,建议只使用公开的API。其他API可能会修改或者改变,而不会通知开发者。

有几本书描述了R API函数。可以参考的包括:

  • Venables WN, Ripley BD (2000) S Programming. Statistics and Computing, pringer-Verlag, New York
  • Gentleman R (2009) R Programming for Bioinformatics. Computer Science and Data Analysis, Chapman & Hall/CRC, Boca Raton, FL
  • Chambers JM (2008) Software for Data Analysis: Programming with R. Statistics and Computing, Springer, Heidelberg, ISBN 978-0-387-75935-7

两个基本的扩展函数:.C().Call()。其中

  • .C()c存在于R语言的早期版本,并被严格限制使用。仅仅用于支持基本的C类型的指针。
  • .Call()被广泛应用。该函数主要操作SEXP 对象,代表S expression object的指针。在R内部,本质上任何对象都是SEXP对象。通过在C++和R之间交换对象,实现对R对象的直接操作。Rcpp只使用.Call()进行内部调用。

Rcpp本质上是建立在R API基础上的一个补充的接口,可以更好的扩展R。利用C++程序,生成更多可供R调用的工具,来提高R编程的效率。

2.4 Rcpp的首次编译

将下列C++代码存为fibonacii.cpp文件。

#include <Rcpp.h>
int fibonacci(const int x) {
  if (x == 0) return(0);
  if (x == 1) return(1);
  return (fibonacci(x - 1)) + fibonacci(x - 2);
}

extern "C" SEXP fibWrapper(SEXP xs) {
  int x = Rcpp::as<int>(xs);
  int fib = fibonacci(x);
  return (Rcpp::wrap(fib));
}

如何编译上述代码?需要进行以下几项准备工作(本文主要针对windows 10系统下):

  • 首先R CMD命令要可以运行。安装最新的Rtools
  • 安装完成后,设置环境变量,在path中添加R主程序所在的路径,本文中用的是R 4.0可执行文件所在的路径,本文所用路径为:C:\Program Files\R\R-4.0.3\bin
  • 设置make.exe路径。在path变量中添加C:\rtools40\usr\bin

注意windows输出的路径是"\“需要替换为”/"。

添加完成后,Win+R,输入cmd,然后在命令行中输入path,查看是否添加成功。

接下来,需要在win 10系统中添加两个环境变量:

  • PKG_CXXFLAGS,将其值设置为:-I/C:/Users/luan_/Documents/R/win-library/4.0/Rcpp/include。这个环境变量表示头文件所在的路径。
  • PKG_LIBS,将其值设置为-L/C:/Users/luan_/Documents/R/win-library/4.0/Rcpp/libs/x64 -lRcpp。这个环境变量表示库文件所在的路径和名称。

其中,C:/Users/luan_/Documents/R/win-library/4.0/Rcpp/为本文Rcpp包所在路径。

完成上述两项工作后,win+R,输入cmd,通过cd命令切换到fibonacci.cpp文件所在的路径,在窗口中输入下边的命令:

R CMD SHLIB fibonacci.cpp

编译和链接代码。

R CMD SHLIB命令执行后,会输出如下信息:

C:/rtools40/mingw64/bin/g++ -std=gnu++11 -shared -s -static-libgcc -o fibonacci.dll tmp.def fibonacci.o -L/C:/Users/luan_/Documents/R/win-library/4.0/Rcpp/libs/x64 -lRcpp -LC:/PROGRA~1/R/R-40~1.3/bin/x64 -lR

命令主要触发两个调用:

  • 将源文件fibonacci.cpp转换为目标文件fibonacci.o;
  • 将目标文件fibonacci.io链接为一个共享库文件fibonacci.dll。

接下来的工作是调用生成的共享链接库fibonacci.dll文件。 在R中加载Rcpp库,通过setwd()切换到fibonacci.dll所在路径,通过dyn.load().Call()进行加载和调用。

library(Rcpp)
setwd("C:/Users/luan_/OneDrive/HugoMini/content/post/code")
dyn.load("fibonacci.dll")
.Call("fibWrapper",10)

输出结果为:

## [1] 55

卸载dll的命令为dyn.unload("fibonacci.dll")

2.5 inline 包

现在再理解起来,比第一章要容易的多了。inline包适合快速开发,因为C++代码可以直接在R代码中书写,直接与R代码一起运行,C++代码的编译、链接和载入在后台进行,特别方便。

inline提供函数cfunction()cxxfunction(),在R会话中直接编译、链接和加载C++函数。 在cxxfunction()函数中,可以设置plugin参数,用来指定头文件和库位置,在本文中,一般会指定为Rcpp。因此,为了简化使用,inline提供一个cxxfunction()的替代函数rcpp(),默认plugin为rcpp。

来看一个示例代码,卷积的计算公式。 卷积好像是一种预测,对不停的信号输入,可以预测其输出。知乎上有个以复利进行解释的的例子,比较容易的理解。https://www.zhihu.com/question/22298352

##定义C++代码
src <- '
Rcpp::NumericVector xa(a);
Rcpp::NumericVector xb(b);
int n_xa =xa.size(), n_xb = xb.size();

Rcpp::NumericVector xab(n_xa + n_xb-1);
for (int i=0; i < n_xa; i++)
  for (int j=0; j < n_xb; j++)
    xab[i+j] += xa[i] * xb[j];
return xab;
'

###定义在R中调用的接口函数
fun <- cxxfunction(signature(a="numeric",b="numeric"),
src,plugin="Rcpp")

#利用rcpp函数定义接口函数
funrcpp <-  rcpp(signature(a="numerci",b="numeric"),src)

##记得在代码开始行加载Rcpp和inline两个包,不然会找不到Rcpp调用的类型和函数
require(Rcpp)
require(inline)
fun(1:4,2:5)
## [1]  2  7 16 30 34 31 20
funrcpp(1:4,2:5)
## [1]  2  7 16 30 34 31 20

函数中添加verbose=TRUE参数可以显示cxxfunction()生成的临时文件和R CMD SHLIB触发的调用。

funverbose <- cxxfunction(signature(a="numeric",b="numeric"),
src,plugin="Rcpp",verbose = TRUE)
##  >> setting environment variables: 
## PKG_LIBS = 
## 
##  >> LinkingTo : Rcpp
## CLINK_CPPFLAGS =  -I"C:/Users/luan_/Documents/R/win-library/3.4/Rcpp/include" 
## 
##  >> Program source :
## 
##    1 : 
##    2 : // includes from the plugin
##    3 : 
##    4 : #include <Rcpp.h>
##    5 : 
##    6 : 
##    7 : #ifndef BEGIN_RCPP
##    8 : #define BEGIN_RCPP
##    9 : #endif
##   10 : 
##   11 : #ifndef END_RCPP
##   12 : #define END_RCPP
##   13 : #endif
##   14 : 
##   15 : using namespace Rcpp;
##   16 : 
##   17 : 
##   18 : // user includes
##   19 : 
##   20 : 
##   21 : // declarations
##   22 : extern "C" {
##   23 : SEXP file9864474b192a( SEXP a, SEXP b) ;
##   24 : }
##   25 : 
##   26 : // definition
##   27 : 
##   28 : SEXP file9864474b192a( SEXP a, SEXP b ){
##   29 : BEGIN_RCPP
##   30 : 
##   31 : Rcpp::NumericVector xa(a);
##   32 : Rcpp::NumericVector xb(b);
##   33 : int n_xa =xa.size(), n_xb = xb.size();
##   34 : 
##   35 : Rcpp::NumericVector xab(n_xa + n_xb-1);
##   36 : for (int i=0; i < n_xa; i++)
##   37 :   for (int j=0; j < n_xb; j++)
##   38 :     xab[i+j] += xa[i] * xb[j];
##   39 : return xab;
##   40 : 
##   41 : END_RCPP
##   42 : }
##   43 : 
##   44 : 
## Compilation argument:
##  C:/PROGRA~1/R/R-34~1.0PA/bin/x64/R CMD SHLIB file9864474b192a.cpp 2> file9864474b192a.cpp.err.txt

2.5.2 使用includes

这个参数,可以让我们使用定义的新class或者struct。

inc <- '
  template <typename T>
  class square : public : std::unary_function<T,T> {
  public:
    T operator()( T t) const { return t*t;}
  };
'

src <- '
  double x = Rcpp::as<double>(xs);
  int i = Rcpp::as<int>(is);
  square<double> sqdbl;
  square<int> sqint;
  Rcpp::DataFrame df =
    Rcpp::DataFrame::create(Rcpp::Named("x", sqdbl(x)),
                            Rcpp::Named("i",sqint(i)))
    return df;
'

fun <- cxxfunction(signature(xs="numeric", is="integer"),
                    body=src, include=inc, plugin="Rcpp")
fun(2.2, 3L)

上述代码定义了一个模板类square,基于unary_function,通过模板化,使输入值和返回值类型相同,可以对不同的数据类型,计算平方和。Rcpp::as<>()转换器,将R类型转为C++类型。