Rcpp — RからC++を使う
プログラムの書き方によって速度やメモリ効率は大きく変わる。 Rでは大抵、生のforループを避けて、R標準のベクトル演算やちゃんとしたパッケージの関数を使っていれば大丈夫。 でも、どうしても、さらに速度を追い求めたい場合にはRcppが有用となる。
長さnの調和級数を求める例:
r_for = function(n) {
s = 0; for (i in seq_len(n)) {s = s + 1 / i}; s
}
r_vec = function(n) sum(1 / seq_len(n))
Rcpp::cppFunction("double rcpp(int n) {
double s = 0; for (int i = 1; i <= n; ++i) {s += 1.0 / i;} return s;
}") # Compilation takes a few seconds here
n = 1000000L
bench::mark(r_for(n), r_vec(n), rcpp(n))[, 1:5]
# expression min median itr/sec mem_alloc
# <bench_expr> <bench_time> <bench_time> <dbl> <bench_bytes>
# 1 r_for(n) 17.89ms 18.32ms 54.73703 0B
# 2 r_vec(n) 3.71ms 3.83ms 244.17061 11.44MB
# 3 rcpp(n) 933.96µs 948.07µs 1038.38453 2.49KB
Documentation
- Project Home: https://www.rcpp.org/
- CRAN: https://cran.r-project.org/package=Rcpp
- Rcpp-JSS-2011.pdf: 原典。
- Rcpp-introduction.pdf: なぜRcppを使うのか。
- Rcpp-attributes.pdf 現在主流となっているRcppの使い方全般。 “Rcpp Attributes” という名前がついていて、 “inline” という古いパッケージのやり方を置き換えたらしい。
- Rcpp-package.pdf: 自作RパッケージでRcppを使う。
- Rcpp-extending.pdf: C++クラスから既存のR型へ、またその逆の変換。
- Rcpp-modules.pdf: C++クラスをRC/S4としてRにexposeする。
- Rcpp-sugar.pdf: ベクトル化とlazy評価が効くRの記法をC++側で使う。
- Rcpp-quickref.pdf
- Rcpp-FAQ.pdf
- GitHub: https://github.com/RcppCore/Rcpp
- 上のPDFは分量が多いわりに意外と網羅的ではない。
ざっくり読んでなんとなく分かってきたら、
さらなるドキュメントを求めてネットの海を彷徨うよりソースコードに当たったほうが早い。特に
inst/unitTests
はかなり参考になる。
- 上のPDFは分量が多いわりに意外と網羅的ではない。
ざっくり読んでなんとなく分かってきたら、
さらなるドキュメントを求めてネットの海を彷徨うよりソースコードに当たったほうが早い。特に
- API: https://dirk.eddelbuettel.com/code/rcpp/html/
- Advanced R: Rewriting R code in C++
- みんなのRcpp and Rcpp for everyone by 津駄@teuderさん
Rスクリプトの途中で使う
ファイルあるいは文字列をコンパイルして使う:
Rcpp::sourceCpp("fibonacci.cpp")
Rcpp::sourceCpp(code='
#include <Rcpp.h>
// [[Rcpp::plugins(cpp14)]]
// [[Rcpp::export]]
int fibonacci(const int x) {
if (x < 1) return 0;
if (x == 1) return 1;
return fibonacci(x - 1) + fibonacci(x - 2);
}
')
fibonacci(9L)
# [1] 34
いろいろな準備を任せて、関数をひとつだけ定義するショートカット:
Rcpp::cppFunction(plugins = c("cpp14"), '
int fibonacci(const int x) {
if (x < 1) return 0;
if (x == 1) return 1;
return fibonacci(x - 1) + fibonacci(x - 2);
}
')
fibonacci(9L)
# [1] 34
Rパッケージで使う
- Rcpp-package.pdf by Dirk Eddelbuettel and Romain François
- tidyverseではcpp11を使うようになったので R Packages (Wickham and Bryan) におけるRcppの扱いは小さい。
準備手順
-
まずRcppコード以外の部分を作っておく。 devtoolsページを参照。
-
usethis::use_rcpp()
を実行して設定を整える。DESCRIPTION
やsrc/.gitignore
などが書き換えられる。 -
R/hello-package.R
に@useDynLib
の設定を追加: (パッケージ名helloを適宜置き換えて)#' @useDynLib hello, .registration = TRUE #' @importFrom Rcpp sourceCpp #' @keywords internal "_PACKAGE"
@importFrom Rcpp sourceCpp
を省くと、パッケージ利用時に'enterRNGScope' not provided by package 'Rcpp'
のようなエラーが出る場合がある (明示的にlibrary(Rcpp)
するなどして既にRcppロード済みの環境では動く)。 -
同じところに
.onUnload
も定義しておく:.onUnload = function(libpath) { library.dynam.unload("hello", libpath) }
すると
unloadNamespace("hello")
したときに共有ライブラリもちゃんと外れるようになる。 ちなみにdevtools::unload()
はこれを省略してもちゃんとリロードしてくれる。 -
外部ライブラリのリンクに関する設定など、 開発者側で指定すべきビルドオプションは
src/Makevars
に指定:CXX_STD=CXX14 PKG_CPPFLAGS=-DSTRICT_R_HEADERS -I/usr/local/include PKG_LIBS=-L/usr/local/lib -Wl,-rpath,/usr/local/lib -lthankyou
STRICT_R_HEADERS
を定義しておくことで余計なマクロ定義を防げる。configure
や CMake を使ってsrc/Makevars.in
から生成する手もある。configure
やcleanup
といったスクリプトはbash拡張を含まない/bin/sh
で実行可能じゃなきゃいけないらしいので、checkbashisms
をインストールしてチェックすることが求められる (brew install checkbashisms
)。CXX_STD=CXX14
が存在しない場合はDESCRIPTION
のSystemRequirements: C++14
が参照されるので、 ほかにsrc/Makevars
を使う用が無い場合はそっちで指定するのが楽。 -
どうしてもユーザ側で指定すべきオプションがある場合は
~/.R/Makevars
に書いてもらう。 例えばMPI依存パッケージをmacOSでビルドでしようとするとclang: error: unsupported option '-fopenmp'
と怒られるのでbrew install llvm
で別のコンパイラを入れて下記のように指定する:LLVM_LOC=/usr/local/opt/llvm CC=$(LLVM_LOC)/bin/clang CXX=$(LLVM_LOC)/bin/clang++
-
src/
以下にソースコードを書く。
ソースコード src/*.cpp
#include <Rcpp.h>
//' First example
//' @param args string vector
//' @export
// [[Rcpp::export]]
int len(const std::vector<std::string>& args) {
return args.size();
}
std::abort()
やstd::exit()
は呼び出したRセッションまで殺してしまう。 例外は投げっぱなしで拾わなくても大丈夫で、std::exception
の派生クラスならwhat()
まで表示してもらえる。- グローバル変数やクラスのstaticメンバは
dyn.unload()
されるまで生き続ける。parallel::mclapply()
とかでフォークした先での変更は子同士にも親にも影響しない。 - 標準出力をRのコンソールに正しく流すには
std::cout
じゃなくてRcpp::Rcout
を使うべし。 とのことなんだけど、その仕事を担ってるのは中のストリームバッファのほうなので、rdbuf()
を使ってバッファを差し替えればRcout
のガワは実は必要ない。 時間があればそのへんの提案と実装をちゃんと送りたいけど… https://github.com/RcppCore/Rcpp/pull/918
詳細
アタリがついてる場合は namespace Rcpp とかからブラウザのページ内検索で探すのが早い。
Rcppで楽ができるとはいえ、R本体の内部情報も知っておいたほうがいい。
C++から直接 R API に触れるべきではないという意見もあって、
R-develで議論になっている。
R APIの例外処理で longjmp
が多用されているため、
RAIIを期待したC++コードはデストラクタが呼ばれなくてバグる危険性が高い、
というのがひとつ大きな問題らしい。
ちゃんと理解しないうちは Rinternals.h
の中身を直接呼ぶのは避けて、
Rcpp::
名前空間内のC++関数だけを使うようにするのがとりあえず安全。
- https://github.com/hadley/r-internals
- https://github.com/wch/r-source/blob/trunk/src/include/Rinternals.h
- https://cran.r-project.org/doc/manuals/r-release/R-ints.html
- https://cran.r-project.org/doc/manuals/r-release/R-exts.html
型
SEXP
: S Expression- Rのあらゆるオブジェクトを表すC言語上の型。
メモリアロケーションやgcへの指示 (
PROTECT
/UNPROTECT
など) が必要。 そういうことは Rcpp が肩代わりしてくれるので基本的には直接触らない。 Rcpp::RObject
SEXP
の thin wrapper であり Rcpp から R の変数を扱う際の基本クラス。 メモリ開放のタイミングは依然としてgc次第なものの、 コード上ではRAIIのような感覚で気楽に使える。Rcpp::Vector<T>
vector/instantiation.h
抜粋:typedef Vector<LGLSXP> LogicalVector; typedef Vector<INTSXP> IntegerVector; typedef Vector<REALSXP> NumericVector; // DoubleVector typedef Vector<STRSXP> StringVector; // CharacterVector typedef Vector<VECSXP> List; // GenericVector
クラスのメンバとして生の配列ではなくそこへの参照を保持する。 しかし
std::vector
とは異なり、 このオブジェクトをコピーしてもメモリ上の中身はコピーされず、 ふたつとも同じ生配列を参照する。C++関数がRから呼ばれるとき
Rcpp::Vector<>
受け取りの場合はうまく参照渡しになるが、const std::vector<>&
受け取りの場合はコピーが発生する。Rcpp::DataFrame
- Rの上では強力だけどC++内では扱いにくい。 出力として使うだけに留めるのが無難。
関数オーバーロードもテンプレートもそのままRにexportすることはできない。 実行時の型情報で振り分ける関数で包んでexportする必要がある。 https://gallery.rcpp.org/articles/rcpp-return-macros/
タグ
[[Rcpp::export]]
- これがついてるグローバル関数は
RcppExport.cpp
を介してライブラリに登録され、.Call(`_{PACKAGE}_{FUNCTION}`)
のような形でRから呼び出せる様になる。 それを元の名前で行えるような関数もRcppExport.R
に自動で定義してもらえる。 [[Rcpp::export(".new_name_here")]]
のように名前を変更することもできる。 ドットで始まる名前にしておけばload_all(export_all=TRUE)
の状態での名前空間汚染を多少調整できる。- Rパッケージの
NAMESPACE
におけるexport()
とは別物。 [[Rcpp::plugins(cpp14)]]
- たぶん
sourceCpp()
とかcppFunction()
で使うための機能で、 パッケージ作りでは効かない。 - ほかに利用可能なものはソースコード
R/Attributes.R
に書いてある。 [[Rcpp::depends(RcppArmadillo)]]
- ほかのパッケージへの依存性を宣言。
たぶんビルド時のオプションをうまくやってくれる。
#include
は自分で。 [[Rcpp::interfaces(r,cpp)]]
Rcpp::export
するとき、どの言語向けにいろいろ生成するか。 何も指定しなければr
のみ。cpp
を指定すると、ほかのパッケージからRcpp::depends
できるようにヘッダーを用意してくれたりするらしい。[[Rcpp::init]]
- これがついてる関数はパッケージロード時に実行される。
[[Rcpp::internal]]
[[Rcpp::register]]
自作C/C++クラスをRで使えるようにする
Rcpp::XPtr<T>
に持たせてlistか何かに入れるか、
“Rcpp Modules” の機能でRC/S4の定義を自動生成してもらう。
ここで説明するのは後者。
Moduleの記述を自分でやらず Rcpp::exposeClass()
に生成してもらう手もある。
-
RcppExports.cpp
に自動的に読み込んでもらえるヘッダー (e.g.,src/{packagename}_types.h
) で自作クラスの宣言とRcpp::as<MyClass>()
/Rcpp::wrap<MyClass>()
の特殊化を行う。#include <RcppCommon.h> RCPP_EXPOSED_CLASS(MyClass); // これで as<MyClass> / wrap<MyClass> の特殊化が定義される // 必ず #include <Rcpp.h> より前に来るように #include "myclass.hpp" // 自作クラスの宣言
-
どこかのソースファイルでモジュールを定義
#include <Rcpp.h>` RCPP_MODULE(mymodule) { Rcpp::class_<MyClass>("MyClass") .constructor<int>() .const_method("get_x", &MyClass::get_x) ; }
-
zzz.R
でモジュールを読み込む。 関数やクラスを全てそのまま公開するか、Module
オブジェクト越しにアクセスさせるようにするか。Rcpp::loadModule("mymodule", TRUE)` # obj = MyClass$new(42L) modulename = Rcpp::Module("mymodule") # obj = mymodule$MyClass$new(42L)
場所は
{packagename}-package.R
とかでもいいけど読まれる順序が重要。setClass("Rcpp_MyClass")
を書く場合にはそれより後で読まれるようにしないとdevtools::load_all()
やdevtools::test()
などリロード後のオブジェクト生成でエラーになる:trying to generate an object from a virtual class
パッケージを読み込むといくつかのRC/S4クラスが定義される。
Rcpp_MyClass
C++Object
を継承した Reference Class (RC)。- S4メソッドをカスタマイズするには明示的に
setClass("Rcpp_MyClass")
したうえでsetMethod("show", "Rcpp_MyClass", \(obj) {})
などとしていく。 C++Object
- R上でC++オブジェクトを扱うための親S4クラス。
Rコンソール上での表示はこれの
show()
メソッドがデフォルトで利用される。C++ object <0x7fd58cfd2f20> of class 'MyClass' <0x7fd59409d1d0>
C++Class
- コンストラクタをR側にexposeするためのクラスで、
MyClass$new(...)
のようにして新規オブジェクトを生成する。 ただしデフォルト引数を扱えないのでファクトリ関数を普通に[[Rcpp::Export]]
したほうが簡単かも。 - staticメソッドも同様に扱えれば一貫性があったんだけど今のところ無理そう。
C++Function
としてならexposeできる。 C++Function
- わざわざModule機能でexposeした関数を扱うS4。
普通に
[[Rcpp::Export]]
する場合と比べたメリットは? Module
environment
を継承したS4。
RC/S4関連文献
?setRefClass
or https://stat.ethz.ch/R-manual/R-devel/library/methods/html/refClass.html- https://adv-r.hadley.nz/oo.html
- https://adv-r.hadley.nz/s4.html
問題点
-
Rcpp ModulesはRCのメソッドにデフォルト引数を持たせることができない。 元のC++クラスのメソッドにデフォルト引数があっても無視。 パッケージロード後、例えば
.onAttach()
の中でMyClass::methods(fun = ...)
などとしてR関数としてメソッドを定義することは可能ではある。 でもそれだとprint(MyClass)
の表示にも追加されずobj$
からの補完候補にも挙がらない。 -
Reference Class はドキュメントを書きにくい。 個々のメソッドの冒頭で書くdocstringは
MyClass$help("some_method")
のようにして確認できるがman/*.Rd
を生成しない。 Roxygenもほとんど助けてくれない。 この状況はR6でもほぼ同じ。 あんまり需要ないのかな。。。 -
結局、オブジェクトを第一引数にとるラッパーR関数をすべてのメソッドに用意して、 そいつにRoxygenコメントを書くのが現状の最適解か。 オブジェクトを第一引数に取るグローバルC++関数を
Rcpp::Export
する手もあって、 そっちのほうがソースコードの冗長性も低く抑えられるけど、 なぜか呼び出しコストが10µs, 2KBくらい余分にかかる。 この時間はnamespace::
有り無しの差と同じくらい。
マクロ
https://dirk.eddelbuettel.com/code/rcpp/html/module_8h.html
RCPP_EXPOSED_AS(MyClass)
as<MyClass>
を定義してくれるマクロ。参照型やポインタ型もやってくれる。RCPP_EXPOSED_WRAP(MyClass)
wrap<MyClass>
を定義してくれるマクロ。RCPP_EXPOSED_CLASS_NODECL(MyClass)
- 上の2つを同時にやってくれるショートカット。
RCPP_EXPOSED_CLASS(MyClass)
- それらの前にさらに
class MyClass;
の前方宣言もする。
cpp11
Rcpp との違い
- header-only なので依存関係のトラブルが少ない。
- コンパイルが速くて省メモリ。
- vectorに変更を加えるには明示的にwritableな型を使う必要があり、 これが自動的にcopy-on-writeしてくれるので、 うっかり参照元オブジェクトまで変更してしまう事故が起こりにくい。
- ALTREPオブジェクトを扱える。
とはいえ新規作成の方法は用意されていない。
R 4.1 までの
Rinternals.h
にあったR_compact_intrange
マクロは R 4.2 から非公開のDefn.h
に移動してしまった。 - Rcpp Modules が無い。
つまり、自作クラスをR側で使うには
cpp11::external_pointer<MyClass>
を受け渡しする関数を自分で用意する必要がある。 - Rcpp Sugar が無い。
つまり、Rのベクトル演算っぽい書き方をC++側でやりたければ自分で関数を書く必要がある。
cpp11::package("base")
から関数を呼べるけどコストを考えるとやや本末転倒。 - Rcpp Attributes
// [[Rcpp::export]]
に比べると[[cpp11::register]]
のサポートは限定的。- C++関数にデフォルト引数をつけてもR関数には反映されない。
- C++関数にRoxygenコメントをつけてもR関数には反映されない。
cpp11test/src
にテストを置く。
Rcpp からの移行
https://cpp11.r-lib.org/articles/converting.html
usethis::use_cpp11()
#include <Rcpp.h>
→#include <cpp11.hpp>
// [Rcpp::export]
→[[cpp11::register]]
- Cheatsheet を見ながらソースコードを書き換え。
R/hello-package.R
を確認。#' @useDynLib hello, .registration = TRUE #' @keywords internal "_PACKAGE"
@import Rcpp
とか@importFrom Rcpp sourceCpp
があれば消す。DESCRIPTION
LinkingTo
:Rcpp
→cpp11
Imports
:Rcpp
→cpp11
R/RcppExports.R
消す。src/RcppExports.cpp
消す。src/Makevars
:PKG_CPPFLAGS=-DSTRICT_R_HEADERS
消す。devtools::clean_dll()
devtools::document()
misc.
cpp11::r_vector<T>
はRcpp::Vector<T>
と同様、 値渡ししても中身はコピーされず参照渡し的な挙動になる。 ただしcpp11では変更不可能。cpp11::writable::r_vector<T>
はRf_shallow_duplicate
をコピーコンストラクタに持つおかげで R と同じような copy-on-write の挙動を示す。 どうしても参照渡ししたい場合はstd::move()
を通す。