跨越R與C++的橋梁:Rcpp
因為寫R擴展的需要,我用Rcpp其實有一陣子了。我一直在強調,把方便和豐富的R環境與系統級語言如C/C++等結合才是適合我現時計算的王道。但R自帶的C/C++的API接口實在是太難用,很是限制人的使用欲望。我試圖把它們用最簡單的方式敘述過,比如可以看這里和這里。但實際應用中無法因繁就簡,還是避免不了寫一大堆重復代碼,記一大堆生僻的API。于是Rcpp成了我的救星,特別是前兩天升級后,我意外地發現了很多新的特性,雖然還沒真正使用,但估計會使得我的工作更有效率。把這個包的幾篇文檔拿來研究了一番,做了些筆記。
Rcpp的終極目標,是通過C++面向對象與模板編程的機制,把幾乎所有R API與數據對象都封裝成類以及類的方法。這樣,寫C++擴展的人只需要了解這些類以及這些類的調用即可,而把接口定義、垃圾回收(gc)、異常捕獲、數據類型的轉換等等對于R/C++交互都是必須的工作都隱藏了起來。新版的Rcpp在效率上更是花了大功夫,一個突出的特點是減少了數據對象的拷貝轉換,直接對原來的R對象進行操作,這樣無論是時間上還是空間上,都獲得了很大的好處(這些額外的耗費我在使用舊版Rcpp時深有體會,新版我還沒深入使用,不知道會提升多少)。這里的論述都是針對新版(目前是0.9.1)Rcpp的,相比而言,舊版的接口真是浮云。
一個例子: Rcpp與基本的R API在寫擴展時的對比
見下圖的兩段代碼:
(a)里的幾個你不熟悉的數據類型與函數都是R API定義的類型與函數,其中包括了R對象的創建、數據類型的轉換、內存對象的保護(預防gc),看起來很別扭,光這些API不知又得耗費你多少的腦容量。(b)就很簡單了,整個就一正常的c++代碼,只是引入了一個Rcpp名空間下的NumericVector類而已。事實上,Rcpp利用類的繼承機制,為幾乎所有的R基本數據類型都建立了一個類來管理,比如這里的NumericVector類就是管理數值型向量數據類型的。
Rcpp的類層次結構
Rcpp管理R對象的基類叫RObject,正如前面所述,可以傳遞一個R對象以構造這個類的實例,舊版的處理會把對象拷貝成一個C++的STL對象,新版則只是簡單地保存了一個對它的引用,類僅充當一個代理的角色,把所有的操作都在內部通過R API來完成。RObject還提供了一些對象通用的方法,如對象屬性查詢方法:isNULL、isObject、isS4;對象屬性管理方法:attributeNames, hasAttribute, attr;slots的管理方法:hasSlot, slot。這些方法對于R用戶來說應該都很熟悉,就不用去查那晦澀難懂的R API函數了。由這個基類派生出來的子類則負責具體對象的特定的處理,如Vector、Matrix、Character、Environment、Function等對象的子類。不同的數據對象可能有不同的數據類型,所以又有不同的類來處理,比如Vector就有IntegerVector, NumericVector, RawVector, LogicalVector, CharacterVector, GenericVector(List), ExpressionVector。
R與C++間的數據類型轉換
雖說Rcpp只保存R對象的一個引用,但兩種不同數據格式間的轉換肯定也是必須的,所以有了Rcpp::wrap與Rcpp::as函數,前者用于把C++對象轉換成R對象,后者則相反。這兩個函數都是用C++的模板元編程技術實現的。這些轉換有時候是隱式調用的,如賦值時兩邊的類型不匹配,就會作出自動轉換。
異常捕獲
由于R與C++使用不同的異常捕獲模型,所以要加以處理,才能在R環境中捕獲到來自C++的異常消息。所以,Rcpp提供了BEGIN_RCPP與END_RCPP這一對宏,只要把有可能出現C++異常的代碼放到這對宏里,就可以在R環境中接收到C++的異常。
性能比較
Rcpp重新設計的一個重要思路就是使得對象的拷貝盡量地少,同時其設計也借鑒了STL的設計原理,比如對iterator的使用。雖然作者對取值operator[]作了多方的優化,但性能卻是不如iterator好。下面的表格是對一個卷積計算多次的計算時間比較。
R API是原生的支持,這里用作參照。可以看到的是舊版的Rcpp實現(最后一行)的性能比原生支持差得甚遠,這是因為有過多的對象拷貝與轉換的緣故。使用了operator[]的實現性能有了很大的提升,但還是比不上原生的支持。但使用了 iterator實現的版本,已經跟原生支持的效率差不多了。更神奇的是,第二行名為Rcpp sugar的實現比原生支持更快。這是一個仍在開發完善中的模塊,但目前已經擁有很多的功能了。這個模塊的目的在于在C++里也能充分地利用R的向量化計算的特性,不但代碼寫起來感覺跟R的思路很類似,效率上也得到了很大的提升。
正在進行中的開發
上面已經提到,Rcpp sugar是一個正在開發完善中的模塊,擁有它之后,你可以在C++層面調用更多的R的函數,使用更多的R的特性。另一個重要的模塊是Rcpp modules,它啟發自Boost.Python模塊,通過這個模塊,要把C++函數導入到R變得更為簡單,只要通過Rcpp的宏RCPP_MODULE把一般的C++函數包裝一下,就可以直接在R環境中導入并使用,完全不需要額外的轉換工作。
Rcpp的作者還在開發一個名為RcppArmadillo的包,Armadillo是一個模板化的C++線性代數包,試圖在效率與易用性上尋求個平衡點,而RcppArmadillo就是一個接口,使得在R中也能使用Armadillo。