R与Cpp混合编程一

Last update: 2021-06-09

Rcpp基本简介

http://arma.sourceforge.net/

Armadillo supports matrices with the following element types: float, double, std::complex, std::complex, short, int, long, and unsigned versions of short, int, long. Support for other types is beyond the scope of Armadillo. (Note: there is an unofficial version of Armadillo which has fixed-point support)

R配置

要正确编译cpp文件,R的安装路径不能有空格,否则会报错如下:

Error in system(cmd, intern = !showOutput) : 'C:/Program' not found

基本介绍

Rcpp是R与C++混合编程的基础接口包,高效计算的R包中大部分代码都不是用R写的,其中C++混合就是实现高效计算的一个好办法。Armadillo当之无愧地是线性代数计算中R与C++混合编程优秀的接口包,为广大科研工作者所常用。

定义C++函数时加vec& x是引用时定义了一个local variable,然后不是传输整个数据到另一个函数中,这样能加快计算速度。这一种思想就是传说中的引用传递,它比指针传递简单,比值传递高效。

h文件和hpp文件常在cpp的脚本开头出现,它是实现调用现有的C++库函数库函数的方法。我们在写R包时常常把多个Cpp文件用到的公共C++函数定义放到单独的一个文件中,然后再用一个hpp头文件去申明这些函数,最后需要调用它们的Cpp脚本中inlude进去便可以使用。

R与Cpp混合编程有许多trick需要格外注意,初学者常常会遭遇许多的坑。在此,我将我遇到的坑都总结下来,希望减少后来者走弯路的可能性。

switch语句

C++中switch的使用需要注意的几点: 只能针对基本数据类型中的整型类型使用switch,这些类型包括int、char等。对于其他类型,则必须使用if语句。 switch()的参数类型不能为实型 。 case标签必须是常量表达式(constantExpression),如42或者’4’。 case标签必须是惟一性的表达式;也就是说,不允许两个case具有相同的值。

Rcpp加速计算

  1. Armadillo中: it is faster to use inv_sympd() instead than inv or .i(). log_det_sympd is faster than log_det.
  2. 有返回值的中间函数,改成void,无返回值,计算更快
  3. 将一个矩阵乘以一个对角矩阵转化为逐元相乘,降低计算复杂度。 比如一个np的矩阵乘以一个pp的对角矩阵,计算复杂度为O(np^2),而用逐元相乘则降低为O(np);当p非常高的时候,能大大降低计算复杂度。
  4. 提前缓存好不需要更新迭代的变量。
  5. 将double换成float类型。

强大的引用传递

引用专递就是传递对象本身,值传递是传递对象的复制品。 打个比方:比如你有一张相片要修改,把相片编辑工具看作函数,把被处理的相片看作参数,那么直接操作相片原件就相当于引用传递,把相片复制一份然后操作这个复制品就叫值传递。

很明显引用传递会影响当作参数的对象,而值传递不会影响当作参数的对象 C++语言中,函数的参数和返回值的传递方式有三种: 值传递、指针传递和引用传递。

以下是“值传递”的示例程序。

由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n, 所以n的值仍然是0。

void Func1(int x)
{
x = x + 10;
}
...
int n = 0;
Func1(n);

cout << "n = " << n << endl; // n = 0

以下是“指针传递”的示例程序。 由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10。

void Func2(int *x)
{
(* x) = (* x) + 10;
}
...
int n = 0;
Func2(&n);

cout << "n = " << n << endl; // n = 10

以下是“引用传递”的示例程序。 由于Func3函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10。

void Func3(int &x)
{
x = x + 10;
}
...
int n = 0;
Func3(n);
cout << "n = " << n << endl; // n = 10

对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。 实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西? 答案是“用适当的工具做恰如其分的工作”。 指针能够毫无约束地操作内存中的任何东西,尽管指针功能强大,但是非常危险。 如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外

c++中使用引用传递来提高效率

我们写一个函数,比如 objclass fun(objclass obj); objclass是类名,obj是对象,fun是函数名。

然后调用此函数,编译器分两个步骤:

1.每次通过值传递的方式给函数传递一个对象时,都会建立一个该对象的拷贝。

2.每次通过值从函数返回一个对象时,也会建立另一个拷贝。

也就是说调用一次此函数,系统会自动建立两次的对象拷贝,然后再调用两次析构函数释放对象的拷贝。

我们知道当调用函数的时候,这些对象被复制到栈中,这样做会费时且占用内存,当使用用户定义的大对象时,这种拷贝带来的内存开销是非常显著的。当时用用户建立的类时,每次生成这种临时的拷贝都要调用一个特殊的构造函数:复制构造函数。当函数返回时,对象的临时拷贝被删除,此时需要调用对象的析构函数。如果返回对象时通过值传递的方式,那么必须建立对象的拷贝,然后再删除。对于很大的对象,调用构造函数和析构函数在速度和内存方面都会造成很大的开销。

改善C++效率的方法: https://blog.csdn.net/u011808673/article/details/80307358?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242

1 尽量减少值传递,多用引用来传递参数。

5 ++i和i++引申出的效率问题

8 减少除法运算的使用

9 将小粒度函数声明为内联函数(inline)

10 与直接初始化对应的是复制初始化,什么是直接初始化?什么又是复制初始化?

头文件的意义

Cpp中有两类文件,一个是cpp源文件.cpp为扩展名,用于写源代码,包括变量和函数定义;一个是头文件.h为扩展名,用于申明变量和函数,不能包含变量和函数的定义。 只能在头文件中写形如:extern int a;和void f();的句子。这些才是声明。如果写上int a;或者void f() {}这样的句子,那么一旦这个头文件被两个或两个以上的.cpp文件包含的话,编译器会立马报错。 “extern int a;”表示申明一个已定义的变量;“void f();”表示申明一个已定义的无返回值的函数f。 但是,有三种情况除外,头文件中可以写const和static对象的定义;头文件中可 以写内联函数(inline)的定义;头文件中可以写类 (class)的定义。

申明函数

头文件中申明函数的语法:

int Dhv2Feature(unsigned char* p_ucData, int iWidth, int iHeight, unsigned short* p_usBlock);
float Dhv2Dhv(unsigned short* sF1, unsigned short* sF2, int iWidth, int iHeight);

另外省略参数名也是可以的:

int Dhv2Feature(unsigned char* , int , int , unsigned short* );
float Dhv2Dhv(unsigned short* , unsigned short* , int , int );

申明变量

头文件中申明函数的语法:

extern int a;

类对象的申明

car.h:
#ifndef __Car_H__
#define __Car_H__
class Wheel;
class Car
{
public:
    Car(int num);
    ~Car();
    void show();
private:
    Wheel* mWheel;
    int mWheelNum;
};
#endif

wheel.h:
#ifndef __Wheel_H__
#define __Wheel_H__
#include <iostream>
using namespace std;
class Wheel
{
public:
    Wheel();
    ~Wheel();
    void show();
};
#endif

定义wheel.cpp

#include "Wheel.h"
Wheel::Wheel()
{
    cout << "Wheel is created" << endl;
}
Wheel::~Wheel()
{
    cout << "Wheel is destroyed" << endl;
}
void Wheel::show()
{
}

如果加入了命名空间,怎么写类声明呢? 1.如果类都在同一命名空间,那直接写在同一namespace里面就可以了

#ifndef __Wheel_H__
#define __Wheel_H__
#include <iostream>
using namespace std;
namespace Gnuser
{
    class Wheel
    {
    public:
        Wheel();
        ~Wheel();
        void show();
    };
}
#endif

#ifndef __Car_H__
#define __Car_H__
namespace Gnuser
{
    class Wheel;
    class Car
    {
    public:
        Car(int num);
        ~Car();
        void show();
    private:
        Wheel* mWheel;
        int mWheelNum;
    };
}
#endif
  1. 现在如果再加一个命名空间,那么可以这样定义
#ifndef __Road_H__
#define __Road_H__
#include <vector>
using namespace std;
namespace Gnuser
{
    class Car;
}
namespace Dxn
{
    class Road
    {
    public:
        Road(int num);
        ~Road();
        void addCar();
    private:
        vector<Gnuser::Car*> mCars;
    };
}
#endif

预防头文件的反复调用

在C++语言编程中,我们经常会接触到头文件,比如说声明类,或者声明命名空间等,而每次在编写xxx.h的头文件时,编程书上都会让我们在代码的前后加上如下的三句代码:

#ifndef XXX_H_
#define XXX_H_
……
#endif

比如“#ifndef Rcpp__sugar__sets_h”表示判断Rcpp文件夹的子文件sugar中的sets.h头文件是否已经被包含,若已经包含进去,则不执行“……”中的申明语句。

更多详情见: https://blog.csdn.net/u012617944/article/details/78405686; https://blog.csdn.net/leowinbow/article/details/82884518;

头文件与同名源文件的关系

头文件和源文件不必同名,只是约定俗成的,为了好记而已。 见 https://blog.csdn.net/lee244868149/article/details/39341751

hpp头文件与h头文件的区别 C++中的.hpp文件

https://blog.csdn.net/acoolgiser/article/details/102832675

命名空间详解

见: https://zhuanlan.zhihu.com/p/126481010 C++中加const与不加const的区别:https://www.cnblogs.com/code-fisher/p/5329026.html

编程注意

易犯错误

  1. 最大值最小值溢出,C++中最小值。非常接近0的溢出为0。比如\(4^{-200}*A %( 2* 4^{-200}*B)\),对于这种就需要先把\(4^{-200}\)约掉后再计算两个矩阵的逐元除法。

2)整除错误:C++中整数除整数等于相除之后再向下取整。如下示例代码:

 U0 = (bx - 1/n *A * tA * a) / as_scalar(1- 1/n * a.t()*tA * a);
## 这里1/n=0 而不等于真正的小数。应该修改成:
 U0 = (bx - 1.0/n *A * tA * a) / as_scalar(1- 1.0/n * a.t()*tA * a);

这是我找了大半天的错误才找到错误的原因!气死我了,好多的坑。我知道这样C++有这个语法规则,但是写代码的时候还是容易疏忽,而且还找不到错误的点在哪里。

  1. 公式中的加号运算错误的写成减法运算,导致出错。这是我容易出错的地方。 例如:
X_tk = X - repmat(U0.t()  + Mu0.row(k)* W0.t(), n, 1);
#错误写成:
X_tk = X - repmat(U0.t()  - Mu0.row(k)* W0.t(), n, 1);
  1. 避免log(a)当a=0报错的情况,可以使用
log(a + (a==0)) # a=0时,值会等于0

函数定义

  1. 可变虚参定义无返回值函数:这里可变虚参指函数的输入虚参数,通过 “type& object”形式使得传入该函数的变量具有可变性质。于是使得无返回值函数具有了返回值函数的能力。例如:
void changeValue(int x){ //若不加取地址符号
  x = x + 1;
}
int y = 1;
changeValue(y); // 只能改变虚参的值,实参变量的值不变。
cout>> y;
// 1
void changeValue(int& x){ 
  x = x + 1;
}
int y = 1;
changeValue(y); //加上取地址符号后,就可以改变实参变量的值了。
cout>> y;   //arma::svd函数就是一个典型的例子。
// 2
  1. 函数返回值为列表的方法:
# 1.
List output;
output["alpha"] = alpha;
output["beta0"] = beta0;
# 2.
List output = List::create(Rcpp::Named("alpha") = alpha,
                               Rcpp::Named("alpha0") = alpha0,
                               Rcpp::Named("beta0") = beta0,
                               Rcpp::Named("sigma2y") = sigma2y,
                               Rcpp::Named("sigma2z") = sigma2z,
                               Rcpp::Named("sigma2beta") = sigma2beta,
                               Rcpp::Named("gam") = gam,
                               Rcpp::Named("loglik_seq") = loglik_out,
                               Rcpp::Named("loglik") = loglik_max,
                               Rcpp::Named("iteration") = Iteration-1);
  1. 字符串划分函数的使用 利用boost.h头文件中的split()函数来划分字符串,它是一个可变实参变量的无返回值函数。
Example:

Input : boost::split(result, input, boost::is_any_of("\t"))
       input = "geeks\tfor\tgeeks"
Output : geeks
        for
        geeks
Explanation: Here in input string we have "geeks\tfor\tgeeks"
and result is a container in which we want to store our result
here separator is "\t".

具体代码:

/ C++ program to split 
// string into substrings 
// which are separated by 
// separater using boost::split 
  
// this header file contains boost::split function 
#include <bits/stdc++.h> 
#include <boost/algorithm/string.hpp> 
using namespace std; 
  
int main() 
{ 
    string input("geeks\tfor\tgeeks"); 
    vector<string> result; 
    boost::split(result, input, boost::is_any_of("\t")); 
  
    for (int i = 0; i < result.size(); i++) 
        cout << result[i] << endl; 
    return 0; 
} 

更多见: https://www.geeksforgeeks.org/boostsplit-c-library/

  1. 字符串转化为数值型atof atof是iostream.h头文件中命名空间std中的函数,它是ascII to float的缩写,它将ascII字符串转换为相应的单精度浮点数,比如传入“1.234”,经过处理后就返回float类型的数1.234 。类似的还有atoi 、atol、itoa、ftoa等等。

  2. 指针和指针变量

  int a ;  //定义int类型变量

 int *p = &a;   //变量 p 是一个 int* 类型的一级指针变量,&是取地址符,p保存了a 的地址

  cout << *p <<endl; //输出 p 指向变量的值,即输出a的值

  cout <<  p << endl; //输出 p 的值,即输出变量a在内存中的地址

  int **q;  //定义二级指针变量

  q = &p; // 二级指针变量q保存了一级指针变量p的地址

  cout << q <<endl; //输出指针变量p在内存中的地址

cout <<  *q << endl; //输出q指向变量的值,即变量p的值,即a的地址

   cout <<  **q << endl; //可以这样理解  cout<<*(*q), 等价于 cout <<*p, 即输出a的值

https://blog.csdn.net/woainilixuhao/article/details/81784124

  1. 时间变量 std命名空间中还有时间类型. std::clock_t a = clock();

快速调用R中现有函数

以glmnet包中的函数为例:

List cv_glmnet(mat x, vec y, double alpha){
  Rcpp::Environment stats("package:stats");
  Rcpp::Function optim = stats["optim"];
  // calling cv.glmnet()
  Environment glmnet("package:glmnet");
  Function f = glmnet["cv.glmnet"];
  return f(Named("x") = x, Named("y") = y, Named("alpha") = alpha);
  
}

具体代码:

library(glmnet)
res <- cv_glmnet(cbind(1:10,1), 1:10, 0.1)
res$lambda.min

控制输出精度

// Program 2.8 Experimenting with floating point output
#include <iostream> // fixed scientific
#include <iomanip> // setprecision()

using std::setprecision;
using std::fixed;
using std::scientific;
using std::cout;
using std::endl;

int main() {
  float value1 = 0.1f;
  float value2 = 2.1f;
  value1 -= 0.09f;                        // Should be 0.01
  value2 -= 2.09f;                        // Should be 0.01

  cout << value1 - value2 << endl; 
  
  cout << setprecision(14) << fixed;      // Change to fixed notation
  cout << value1 - value2 << endl;        // Should output zero

  cout << setprecision(2) << scientific;  // Set scientific notation
  cout << value1 - value2 << endl;        // Should output zero

  return 0;
}

↑ 返回目录页