開始使用 Rcpp
Rcpp
的 cppFunction
是一個可以讓您將 C++ 程式碼直接內崁至 R 程式碼中的包裝函數,其用法非常簡單,在詳細介紹之前,我們先來看一個典型的範例,先讓大家對 Rcpp 的使用有一個初步的概念。
首先將 R 程式中需要大量運算的部分,改以 C++ 語言來撰寫:
int cpp_add(int x, int y, int z) { int sum = x + y + z; return sum; }
這個 C++ 函數是傳入 x
、y
與 z
三個整數,然後計算它們的總和(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 的使用方式相當簡潔,不需要任何手動編譯的過程。
以下我們將詳細介紹各種參數類型的用法,包含各類型的純量、向量、矩陣的輸入與輸出方式。
無輸入、純量輸出
一個沒有輸入值、只有單一傳回值的 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
),而輸入參數可使用的變數類型跟傳回值可使用的變數類型相同(double
、int
等)。
這裡我們用到一個 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
,並且使用 NumericVector
的 size
方法函數取得向量的長度,接著宣告一個用來儲存總和值的 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)