開始使用 Rcpp

RcppcppFunction 是一個可以讓您將 C++ 程式碼直接內崁至 R 程式碼中的包裝函數,其用法非常簡單,在詳細介紹之前,我們先來看一個典型的範例,先讓大家對 Rcpp 的使用有一個初步的概念。

首先將 R 程式中需要大量運算的部分,改以 C++ 語言來撰寫:

int cpp_add(int x, int y, int z) {
  int sum = x + y + z;
  return sum;
}

這個 C++ 函數是傳入 xyz 三個整數,然後計算它們的總和(sum),最後將計算結果傳回。

撰寫好 C++ 的計算函數之後,直接將 C++ 的整個程式碼內容當成參數,傳遞給 R 的 cppFunction 函數:

cppFunction('int cpp_add(int x, int y, int z) {
  int sum = x + y + z;
  return sum;
}')

cppFunction 會對這段 C++ 程式碼自動進行編譯,並且載入至目前的 R 環境中,若 C++ 的程式碼沒有錯誤的話,這時候在 R 中就會建立好一個剛剛 C++ 中所定義的函數:

cpp_add
function (x, y, z) 
.Primitive(".Call")(, x, y, z)

而這個時候我們就可以在 R 中直接使用這個以 C++ 撰寫的 cpp_add 函數了:

cpp_add(1, 2, 3)
[1] 6

以上就是一個簡單的 Rcpp 使用範例,從這個範例您可以看得出來 Rcpp 的使用方式相當簡潔,不需要任何手動編譯的過程。

由於使用 Rcpp 的同時也必須撰寫一些 C++ 程式,若事先對於 C++ 語言有基本的認識會有很大的幫助。

以下我們將詳細介紹各種參數類型的用法,包含各類型的純量、向量、矩陣的輸入與輸出方式。

無輸入、純量輸出

一個沒有輸入值、只有單一傳回值的 R 函數是這樣寫的:

one <- function() 1L

而若將這個 R 函數以 C++ 改寫,則會像這樣:

int one() {
  return 1;
}

這裡的 int(integer)是指定此函數的傳回值型態為整數,而 return 則是指定實際要傳回的數值。

在 R 函數中可以省略 return 這一行,以最後一個運算結果作為函數傳回值,但是在 C++ 中則不可省略,一定要使用 return 明確指定函數的傳回值。另外 C++ 中的每一行運算式結尾都一定要加上分號(;),不可省略。

在 R 中我們可以將這個 C++ 函數透過 cppFunction 編譯並載入使用:

cppFunction('int one() {
  return 1;
}')
one()
[1] 1

在使用 cppFunction 編譯 C++ 函數時,它會自動在 R 中建立一個相同名稱的 R 函數,以 .Call 呼叫這個編譯好的 C++ 函數,使用者不需要自行建立這個 R 函數。

C++ 函數在定義時需要明確指定其傳回值的型態,若傳回值為簡單的純量(scalar),可以使用 C++ 內建的資料型別:

C++ 語法 說明
double 浮點數
int 整數
std::string 字串
bool 布林值

例如一個傳回字串的 C++ 函數就可以這樣寫:

std::string my_name() {
  return "G. T. Wang";
}

使用 cppFunction 編譯並載入使用:

cppFunction('std::string my_name() {
  return "G. T. Wang";
}')
my_name()
[1] "G. T. Wang"

純量輸入、純量輸出

下面這個 R 函數是一個以勾股定理計算直角三角形斜邊長度的函數,輸入三角形的勾長與股長,傳回弦長(斜邊的長度):

hypotenuse <- function(a, b) {
  sqrt(a * a + b * b)
}

若以 C++ 改寫,則為:

double hypotenuse(double a, double b) {
  return sqrt(a * a + b * b);
}

C++ 函數的輸入參數與傳回值一樣都需要明確指定變數的類型,由於在計算斜邊長度時,會需要浮點運算,所以這裡我們將輸入與輸出的變數都宣告為浮點數(double),而輸入參數可使用的變數類型跟傳回值可使用的變數類型相同(doubleint 等)。

這裡我們用到一個 C++ 的平方根函數 sqrt,其使用方式跟 R 中的 sqrt 相同,可計算傳入數值的平方根。

使用 cppFunction 編譯並載入使用:

cppFunction('double hypotenuse(double a, double b) {
  return sqrt(a * a + b * b);
}')
hypotenuse(3, 4)
[1] 5

向量輸入、純量輸出

C++ 語言的一個很大的優勢就是它的迴圈計算速度比 R 快很多,一個典型的用法是將 R 中的向量傳入 C++ 中進行運算,最後再傳回運算的結果。下面這個例子是一個計算總和的 R 函數:

r_sum <- function(x) {
  total <- 0
  for (i in seq_along(x)) {
    total <- total + x[i]
  }
  total
}

若要將這個 R 函數以 C++ 改寫,則會變成:

double cpp_sum(NumericVector x) {
  int n = x.size();
  double total = 0;
  for(int i = 0; i < n; ++i) {
    total += x[i];
  }
  return total;
}

由於這裡傳入的參數 x 會是一個 R 的向量,在 C++ 中純量與向量是不同的資料型別,Rcpp 針對 R 的幾種向量特別定義了一些 C++ 的類別供使用者使用,這裡我們將 x 的型別宣告為浮點數向量 NumericVector,並且使用 NumericVectorsize 方法函數取得向量的長度,接著宣告一個用來儲存總和值的 total 變數(由於我們要計算浮點數向量的總和,因此將 total 變數宣告為浮點數 double),然後進行後續的 for 迴圈運算,最後將計算完成的總和值傳回。

以下是各種 R 向量所對應的 C++ 類別:

C++ 類別 說明
NumericVector 浮點數向量
IntegerVector 整數向量
CharacterVector 字元向量
LogicalVector 邏輯向量

使用 cppFunction 編譯並載入使用:

cppFunction('double cpp_sum(NumericVector x) {
  int n = x.size();
  double total = 0;
  for(int i = 0; i < n; ++i) {
    total += x[i];
  }
  return total;
}')

我們可以使用 R 的 microbenchmark 套件來測試 R 與 C++ 之間的迴圈運算效能差異:

library(microbenchmark)
x <- runif(1e3)
x.benchmark <- microbenchmark(
  sum(x),
  cpp_sum(x),
  r_sum(x)
)
x.benchmark
Unit: microseconds
       expr     min       lq      mean   median       uq      max neval
     sum(x)   1.131   1.3255   1.61292   1.5745   1.7285    3.708   100
 cpp_sum(x)   2.832   3.2555   4.20410   3.9425   4.7900   14.222   100
   r_sum(x) 344.723 389.2560 444.48158 414.5810 442.0710 1255.476   100

這裡我們測試了一般的 R 函數(r_sum)、C++ 函數(cpp_sum)以及 R 內建的向量化函數(sum),內建的向量化函數執行效率是最好的,而我們自己定義的 C++ 函數稍微遜色一些,而普通 R 函數的執行速度則是比我們定義的 C++ 函數慢了百倍以上。

畫出測試結果的小提琴圖(violin plot):

library(ggplot2)
autoplot(x.benchmark)
rcpp-package-tutorial-microbenchmark-1

測試結果小提琴圖