课堂演讲:R语言与CPP混合编程课后学习汇报。
1)、关于本次主题汇报的想法诞生?
从自身经历角度:
1、个人对C、C++、Linux操作系统有简单的了解,R虽然为解释型语言,但同属于计算机编程语言。
2、已知存在混合编程的情景,最常见的属于C/C++混合编程,比如Linux操作系统主要是通过C和C++编写的。这个例子相对缺陷在于:C++是基于C的改进,能使用C的基本语法。但是,多语言混合编程也存在于python、Java等各种语言间。
3、既然正在了解R语言,那么正好可以探索一下其与C/C++的混合编程,也是熟悉对这些语言的使用。
从R语言自身来看:
关于虚拟内存: 属于计算机操作系统相关的知识,我们的程序可存储于各种外储中,比如磁盘,程序运行需要加载到内存,而加载到内存中的程序称之为进程。CPU对进程有一套完整的调度模式,虚拟空间是操作系统为了更好的管理物理内存而为进程设置的一套体系。
关于RCPP: 这是接下来探讨的内容。
1)、是什么
1、Rcpp包是一个打通R语言和C++语言的通信组件包,可以把C++代码与R程序连接在一起,且不用关心C++中那套繁琐的编译、链接、接口问题。
2、R语言和C++语言的数据类型是不经相同,通过Rcpp包能够对它们进行完整的映射转换。
3、Rcpp支持把C++代码写在R源程序文件内, 执行时自动编译连接调用; 也支持把C++代码保存在单独的源文件中, 执行R程序时自动编译连接调用。
2)、有什么用
1、其最大的用途之一在于提高运行效率,可以把R代码中遇到运行速度瓶颈部分改写成C++代码。
2、C++或C提供了各种方便使用的函数库,Rcpp可支持这些外来函数库的调用。
1)、如何安装Rcpp
> install.packages("Rcpp");
WARNING: Rtools is required to build R packages but is not currently installed. Please download and install the appropriate version of Rtools before proceeding:
https://cran.rstudio.com/bin/windows/Rtools/
trying URL 'https://cran.rstudio.com/bin/windows/contrib/4.2/Rcpp_1.0.9.zip'
Content type 'application/zip' length 2842384 bytes (2.7 MB)
downloaded 2.7 MB
程序包‘Rcpp’打开成功,MD5和检查也通过
下载的二进制程序包在
C:\Users\无咎\AppData\Local\Temp\RtmpMnQcFB\downloaded_packages里
>
2)、如何安装RTools
原因说明: 由于涉及到编译, 所以Rcpp比一般的扩展包有更多的安装要求: 除了要安装Rcpp包之外, 还需要安装RTools包。
RTools包: 用于C, C++, Fortran程序编译链接的开发工具包。
trying URL 'https://cran.rstudio.com/bin/windows/Rtools/rtools42/files/rtools42-5355-5357.exe'
Content type 'application/x-msdownload' length 482123824 bytes (459.8 MB)
downloaded 459.8 MB
相关问题: RStudio自己的自动安装RTools的功能可能有问题, 安装后不能正常编译含有Rcpp的Rmd文件。 按照RTools 4.0的要求, 还要在自己的“我的文档”文件夹中生成名为.Renviron的如下文本文件:
这里采用非手动配置方式:进入R Studio里,运行以下代码即可。
writeLines('PATH="${RTOOLS40_HOME}\\usr\\bin;${PATH}"', con = "~/.Renviron")
运行后关闭RStudio,重新打开并运行下述代码Sys.which("make")
,检测是否配置成功:
> Sys.which("make")
make
"C:\\RBUILD~1\\4.2\\usr\\bin\\make.exe"
如上述可看到成功配置。
要求: 用C++编写一个能打印“hello world”的函数,在R中调用这个函数。
1)、如何在Rstudio中建立一个C++文件:
文件打开:
这里的英文做了详细解释:可以阅读并演示。
下面我们来演示自己的函数,在CPP文件中编写如下代码:
#include
#include
#include //头文件:里面包含各种函数实现的声明、定义
using namespace Rcpp; //命名空间:起限制作用,能解决名称冲突问题。
using namespace std;//这两句的含义是展开Rcpp、std这两个空间中所包含的内容。
// [[Rcpp::export]]
string HelloByCpp(string s)
{
cout << "hello " << s << endl; //cout、endl存在于std中的对象
return s;
}
/*** R
HelloByCpp("world")
*/
在控制台运行下述代码sourceCpp(file = "HelloByCpp.cpp")
表示对相关cpp文件进行编译,转换成R可以调用的同名R函数。
演示结果如下:
> sourceCpp(file = "HelloByCpp.cpp");
> HelloByCpp("world")
hello world
[1] "world"
> HelloByCpp("R");
hello R
[1] "R"
> HelloByCpp("string!");
hello string!
[1] "string!"
解释说明:
1、编译、链接、导入等过程在后台由Rcpp控制自动进行的, 不需要用户去设置编译环境, 也不需要用户执行编译、链接、导入R的工作。
2、同样的,如果上述的代码指令在R中频繁使用,我们可以将其保存为.R的文件。
3)、R中调用CPP实现:
下述R代码,其中有一部分C++代码, 用cppFunction转换成了R可以调用的同名R函数。
> cppFunction(code = '
+ int fib(const int n)
+ {
+ if(n <= 2)
+ return 1;
+ return fib(n-1)+fib(n-2) ;
+ }
+ ');
> fibonacci(2);
[1] 1
> fibonacci(4);
[1] 3
> fibonacci(5);
[1] 5
1)、一些说明:
需要说明的是,上述演示是为了学习了解RCPP包的基础使用方式,像hello world这样的程序与其大费周章去调用CPP,不如直接在R中编写一个简单的函数,也能达到相同效果。
R中直接编写函数实现hello word打印:
> hello <-function(s){
+ print(s);
+ }
> hello ("hello world");
[1] "hello world"
> hello ("hello R");
[1] "hello R"
> hello ("hello vim");
[1] "hello vim"
R中直接编写函数实现斐波那契数递归:
> Fib <- function(n){
+ if(1 == n | 2 == n)
+ {
+ return (1);
+ }else{
+ return (Fib(n-1)+Fib(n-2));
+ }
+ };
+ > Fib(1);
[1] 1
> Fib(2);
[1] 1
> Fib(3);
[1] 2
> Fib(5);
[1] 5
2)、运行速度测试:
就如前文所言,在R中使用C++是为了提高运行速度以及相关库函数使用。在编写函数时我们,有时我们需要遍历数据,或多次循环、递归等。这些是占据进程运行时间的缘由之一。这里我们简单的做个关于运行速度测试例子:
关于演示实例:
这里我们就以输出第40个斐波那契数为例。因为递归是这种嵌套式写法很方便进行检测。
Fib
:用R语言编写的斐波那契数递归函数。
> Fib <- function(n){
+ if(1 == n | 2 == n)
+ {
+ return (1);
+ }else{
+ return (Fib(n-1)+Fib(n-2));
+ }
+ };
fib
用C++编写的斐波那契数递归函数。
+ int fib(const int n)
+ {
+ if(n <= 2)
+ return 1;
+ return fib(n-1)+fib(n-2) ;
+ }
关于测试结果:
这里使用的是system.time()
函数,用于查看系统运行时间,它的时间由三个成分组成。
“用户”:是消耗在应用程序(非操作系统部分)执行的时间。
“系统”:是底层操作系统执行(例如磁盘读写等)部分的时间,
“流逝”:是经过的总时间(可以认为是前两者的总和)。
> system.time(fib(40));
用户 系统 流逝
0.14 0.00 0.14
> system.time(Fib(40));
用户 系统 流逝
110.67 0.04 111.61
上述结果以秒为单位,可看到我们使用C++来处理相关数据,它几乎立马就能得到运行结果。但如果使用R来处理数据,计算40个斐波那契数大约需要2min。
有关运行速度的问题,这里我们再来演示一下,这次换一个相对较大的数据,考虑到R运行速度问题,这里以C++演示:
可以看到使用C++相关编译器,其输入数字,大约到40左右程序差不多就运行不出来了,原因就和上述R中提及的问题一样,涉及存储空间问题,以及递归的深度嵌套效率。
#include
#include
using namespace std;
unsigned int fib(const unsigned int n)
{
if (n <= 2)
return 1;
return fib(n - 1) + fib(n - 2);
}
int main()
{
size_t begin1 = clock();
unsigned int result1 = fib(10);
size_t end1 = clock();
cout << "result1:" << result1 << endl;
cout << "fib(10)-time:" << end1 - begin1 << endl << endl << endl;
size_t begin2 = clock();
unsigned int result2 = fib(20);
size_t end2 = clock();
cout << "result2:" << result2 << endl;
cout << "fib(20)-time:" << end2 - begin2 << endl << endl << endl;
size_t begin3 = clock();
unsigned int result3 = fib(30);
size_t end3 = clock();
cout << "result3:" << result3 << endl;
cout << "fib(30)-time:" << end3 - begin3 << endl << endl << endl;
size_t begin4 = clock();
unsigned int result4 = fib(40);
size_t end4 = clock();
cout << "result4:" << result4 << endl;
cout << "fib(40)-time:" << end4 - begin4 << endl << endl << endl;
size_t begin5 = clock();
unsigned int result5 = fib(50);
size_t end5 = clock();
cout << "result5:" << result5 << endl;
cout << "fib(50)-time:" << end5 - begin5 << endl << endl << endl;
size_t begin6 = clock();
unsigned int result6 = fib(100);
size_t end6 = clock();
cout << "result6:" << result6 << endl;
cout << "fib(100)-time:" << end6 - begin6 << endl << endl << endl;
size_t begin7 = clock();
unsigned int result7 = fib(500);
size_t end7 = clock();
cout << "result7:" << result7 << endl;
cout << "fib(500)-time:" << end7 - begin7 << endl << endl << endl;
size_t begin8 = clock();
unsigned int result8 = fib(1000);
size_t end8 = clock();
cout << "result8:" << result8 << endl;
cout << "fib(1000)-time:" << end8 - begin8 << endl << endl << endl;
return 0;
}
1)、Rcpp属性:
Rcpp属性(attributes):用来简化把C++函数变成R函数的过程。 做法是在C++源程序中加入一些特殊注释, 利用其指示自动生成C++与R的接口程序。 属性是C++11标准的内容。
一些解释说明:
1、在C++中,提供Rcpp::export
标注要输出到R中的C++函数。
2、在R中,提供sourceCpp()
, 用来自动编译连接保存在文件或R字符串中的C++代码, 并自动生成界面程序把C++函数转换为R函数。
3、在R中,提供cppFunction()
函数, 用来把保存在R字符串中的C++函数自动编译连接并转换成R函数。 提供evalCpp()函数, 用来把保存在R字符串中的C++代码片段自动编译连接并执行。
4、用特殊注释//[[Rcpp::export]]
,说明某C++函数需要在编译成动态链接库时, 把这个函数导出到链接库的对外可见部分。通俗来讲就是,在sourceCpp读取代码解析的过程中,被这条语句注释的函数能够在R里面调用。
5、/*** R */
:里面可以放R代码,会在c++代码编译结束后运行,常用于代码测试。
说明:受制于演讲时限,此篇文章只是简单进行抛砖引玉,若对R与C++交互使用感兴趣,可以参考下述提供的网站,这里也有一本与之有关的书:《Rcpp:R 与 C++ 的无缝整合》。
相关参考:
1、R语言教程
2、R的极客理想系列文章
3、《R语言实战》