Heavy Watal

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

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パッケージで使う

準備手順

ソースコード 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();
}

詳細

アタリがついてる場合は namespace Rcpp とかからブラウザのページ内検索で探すのが早い。

Rcppで楽ができるとはいえ、R本体の内部情報も知っておいたほうがいい。 C++から直接 R API に触れるべきではないという意見もあって、 R-develで議論になっている。 R APIの例外処理で longjmp が多用されているため、 RAIIを期待したC++コードはデストラクタが呼ばれなくてバグる危険性が高い、 というのがひとつ大きな問題らしい。 ちゃんと理解しないうちは Rinternals.h の中身を直接呼ぶのは避けて、 Rcpp:: 名前空間内のC++関数だけを使うようにするのがとりあえず安全。

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() に生成してもらう手もある。

  1. 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"
    // 自作クラスの宣言
    
  2. どこかのソースファイルでモジュールを定義

    #include <Rcpp.h>`
    
    RCPP_MODULE(mymodule) {
      Rcpp::class_<MyClass>("MyClass")
        .constructor<int>()
        .const_method("get_x", &MyClass::get_x)
      ;
    }
    
  3. 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関連文献

問題点

マクロ

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

https://cpp11.r-lib.org/

Rcpp との違い

Rcpp からの移行

https://cpp11.r-lib.org/articles/converting.html

misc.