この文書は,Winston Chang によるRパッケージ R6
(version 2.2.0) のビネット "R6 and Reference class performance tests" の日本語訳です.
License: MIT
関連文書
この文書では,Rの参照クラスのメモリ負荷と速度を,R6クラスと単純な環境に対して比較します.大抵の用途においてR6と参照クラスは同等の機能を持っていますが,これから見るようにR6クラスの方が高速で軽量です.
この文書では,参照クラスをR6クラスに対して(様々なバリエーションで)テストします.単純な参照オブジェクト(関数呼び出しで作成した環境)に対してもテストします.
まずは以下で使用するパッケージをロードしましょう.
library(microbenchmark)
options(microbenchmark.unit = "us")
library(pryr) # object_size 関数用
library(R6)
クラス定義
参照クラスやR6,それから単純な環境を用いて,多数のクラスやクラスに類似したものを定義することから始めます.これらは関数によって直接作成できます.R6にはオブジェクトのサイズに影響する多くのオプションがあるので,様々なバリエーションを使うことにします.これらのクラスは以下で速度とメモリのテストに使用されます.退屈なコードばかりなのでテスト結果まで飛ばしてもかまいません.
これらのクラスはすべて共通の特性を備えています.
- 数値を含む
x
という名前のフィールド -
x
の値の初期化方法 -
x
の値を取得するためのgetx
という名前のメソッド -
x
の値をインクリメントするためのinc
という名前のメソッド
これらのフィールドやメソッドには$
演算子でアクセスします.仮にobj
という名前のオブジェクトがあれば,obj$x
やobj$getx()
が使えるでしょう.
Rの参照クラス
RC <- setRefClass("RC",
fields = list(x = "numeric"),
methods = list(
initialize = function(x = 1) .self$x <- x,
getx = function() x,
inc = function(n = 1) x <<- x + n
)
)
参照クラスでは,オブジェクト自身を指し返すように束縛されている名前を.self
といいます.メソッド内での代入は.self
を用いて.self$x <- 10
のように行えます.あるいは<<-
を用いてx <<- 10
のようにもできます.
オブジェクトを作成するには,単にクラスの$new()
を呼び出します.
RC$new()
#> Reference class object of class "RC"
#> Field "x":
#> [1] 1
R6クラス
R6クラスの作成方法は参照クラスと同様です.ただしフィールドとメソッドを分ける必要がなく,またフィールドの型は指定できません
R6 <- R6Class("R6",
public = list(
x = NULL,
initialize = function(x = 1) self$x <- x,
getx = function() self$x,
inc = function(n = 1) self$x <- x + n
)
)
参照クラスでは.self
を使いますが,R6クラスでは(先頭のピリオドのない)self
を使います.参照クラスと同様,オブジェクトは$new()
を呼んでインスタンス化します.
R6$new()
#> <R6>
#> Public:
#> clone: function (deep = FALSE)
#> getx: function ()
#> inc: function (n = 1)
#> initialize: function (x = 1)
#> x: 1
本質的には,R6オブジェクトは環境の集まりをあるやり方で構造化したものにすぎません.R6オブジェクトのフィールドとメソッドはパブリック環境に束縛されています(すなわち,パブリック環境に名前を持ちます).これとは別にメソッドのエンクロージング環境があります.(メソッドはself
という名前が束縛された環境で実行されます.self
はパブリック環境への単なる参照です.)
R6クラス(class
属性なし)
デフォルトでは,R6オブジェクトにはclass
属性が付加されます.この属性の付加によって,わずかに性能が損なわれます.なぜかというと,オブジェクトに対して$
が使用されると,RはS3のディスパッチを試みるからです,
class=FALSE
とすればclass
属性なしのオブジェクトを生成することができます.
R6NoClass <- R6Class("R6NoClass",
class = FALSE,
public = list(
x = NULL,
initialize = function(x = 1) self$x <- x,
getx = function() self$x,
inc = function(n = 1) self$x <- self$x + n
)
)
class
属性がないと,オブジェクトに対してS3メソッドのディスパッチができないことに注意してください.
R6クラス(移植不可)
デフォルトでは,R6オブジェクトは移植可能です.つまり異なるパッケージにあるクラス間で継承ができます.しかし,このためにメンバにアクセスするのにself$
やprivate$
の使用も必要となり,性能では少々損失を被ることになります.
portable=FLASE
とすれば,メンバにはself$
なしでアクセスでき,代入は<<-
で行えます.
R6NonPortable <- R6Class("R6NonPortable",
portable = FALSE,
public = list(
x = NULL,
initialize = function(value = 1) x <<- value,
getx = function() x,
inc = function(n = 1) x <<- x + n
)
)
R6クラス(cloneable=FALSE
)
デフォルトではR6オブジェクトはclone()
メソッドを持ちますが,これはかなり大きな関数です.この機能が不要な場合にはcloneable=FALSE
とすればメモリを節約できます.
R6NonCloneable <- R6Class("R6NonCloneable",
cloneable = FALSE,
public = list(
x = NULL,
initialize = function(x = 1) self$x <- x,
getx = function() self$x,
inc = function(n = 1) self$x <- self$x + n
)
)
R6クラス(class
属性なし,移植不可,クローン不可)
比較のために,class
属性を持たず,移植不可で,クローン不可能なR6クラスを使います.これは余計なものを可能な限り取り除いたR6オブジェクトです.
R6Bare <- R6Class("R6Bare",
portable = FALSE,
class = FALSE,
cloneable = FALSE,
public = list(
x = NULL,
initialize = function(value = 1) x <<- value,
getx = function() x,
inc = function(n = 1) x <<- x + n
)
)
R6クラス(パブリックメンバとプライベートメンバあり)
これはパブリックメンバとプライベートメンバを持つバージョンです.
R6Private <- R6Class("R6Private",
private = list(x = NULL),
public = list(
initialize = function(x = 1) private$x <- x,
getx = function() private$x,
inc = function(n = 1) private$x <- private$x + n
)
)
このR6オブジェクトは,self
だけでオブジェクト内の全ての項目にアクセスすることはできず,(パブリックな項目を指す)self
とprivate
を持ちます.
R6Private$new()
#> <R6Private>
#> Public:
#> clone: function (deep = FALSE)
#> getx: function ()
#> inc: function (n = 1)
#> initialize: function (x = 1)
#> Private:
#> x: 1
R6クラス(パブリックメンバとプライベートメンバあり,class
属性なし,移植不可,クローン不可)
比較のため,class
属性を持たず,移植不可で,クローン不可能なバージョンを加えます.
R6PrivateBare <- R6Class("R6PrivateBare",
portable = FALSE,
class = FALSE,
cloneable = FALSE,
private = list(x = NULL),
public = list(
initialize = function(x = 1) private$x <- x,
getx = function() x,
inc = function(n = 1) x <<- x + n
)
)
環境(関数呼び出しで作成,class
属性あり)
Rでは,環境は参照渡しされます.参照渡しのオブジェクトを作るには,関数実行によって作成される環境を使うのが簡単です.
FunctionEnvClass <- function(x = 1) {
inc <- function(n = 1) x <<- x + n
getx <- function() x
self <- environment()
class(self) <- "FunctionEnvClass"
self
}
x
は関数の本体では宣言されていませんが,関数の引数になっているので,環境に捕捉されます.
ls(FunctionEnvClass())
#> [1] "getx" "inc" "self" "x"
こうして作成したオブジェクトは,上で作成したR6
のジェネレータによく似ています.
環境(関数呼び出しで作成,class
属性なし)
class
属性をなくして,self
オブジェクトもなくすことで,前述のものよりさらに単純な種類の参照オブジェクトを作ることができます.
FunctionEnvNoClass <- function(x = 1) {
inc <- function(n = 1) x <<- x + n
getx <- function() x
environment()
}
これはいくつかのオブジェクトを含む単なる環境です.
ls(FunctionEnvNoClass())
#> [1] "getx" "inc" "x"
テスト
microbenchmark()
による時間の計測結果はすべてマイクロ秒で報告されます.結果の中で最も役立つのはたぶん中央値でしょう.
メモリ負荷
各オブジェクトのひとつのインスタンスはどれだけメモリを使用するのでしょうか.また,オブジェクトを追加するとどれだけメモリを使用するのでしょうか.オブジェクトのサイズを計算するにはobj_size()
とobj_sizes()
という関数を使います(関数定義はこの文書の最下部にあります).
それぞれの種類のオブジェクトのサイズ(バイト単位)は以下の通りです1.
sizes <- obj_sizes(
RC$new(),
R6$new(),
R6NoClass$new(),
R6NonPortable$new(),
R6NonCloneable$new(),
R6Bare$new(),
R6Private$new(),
R6PrivateBare$new(),
FunctionEnvClass(),
FunctionEnvNoClass()
)
sizes
#> one incremental
#> RC$new() 461168 1368
#> R6$new() 56608 1008
#> R6NoClass$new() 57312 896
#> R6NonPortable$new() 56200 952
#> R6NonCloneable$new() 13608 896
#> R6Bare$new() 12800 728
#> R6Private$new() 57512 1120
#> R6PrivateBare$new() 13808 840
#> FunctionEnvClass() 10696 624
#> FunctionEnvNoClass() 9272 512
結果を以下にプロットしました.プロットのx軸のスケールが大きく異なっていることに注意してください.
色々なクラスのひとつめのインスタンスについて,まず以下のことが分かります.参照クラスは大量のメモリを消費すること.R6オブジェクトに対して最も影響のあるオプションはcloneable
であること.clone()
メソッドなしだとメモリが約40kB節約されています.
各クラスの追加されたインスタンスについては,異なる種類のクラスの間で,ひとつめのインスタンスほどの違いはありません.
参照クラスは大量のメモリを占めるように見えましたが,その大部分は複数の参照クラスで共有されるものです.別の参照クラスのオブジェクトを追加しても,それほど多くのメモリは必要としません(約38kB).
RC2 <- setRefClass("RC2",
fields = list(x = "numeric"),
methods = list(
initialize = function(x = 2) .self$x <<- x,
inc = function(n = 2) x <<- x * n
)
)
# RCオブジェクトに加えて,新しく作ったRC2オブジェクトのサイズを計算
as.numeric(object_size(RC$new(), RC2$new()) - object_size(RC$new()))
#> [1] 37344
オブジェクトのインスタンス化の速度
各オブジェクトを作成するのには,どれくらい時間がかかるのでしょうか.以下に,かかった時間の中央値をマイクロ秒で示します.
# microbenchmarkの結果から中央値を取り出す関数
mb_summary <- function(x) {
res <- summary(x, unit="us")
data.frame(name = res$expr, median = res$median)
}
speed <- microbenchmark(
RC$new(),
R6$new(),
R6NoClass$new(),
R6NonPortable$new(),
R6NonCloneable$new(),
R6Bare$new(),
R6Private$new(),
R6PrivateBare$new(),
FunctionEnvClass(),
FunctionEnvNoClass()
)
speed <- mb_summary(speed)
speed
#> name median
#> 1 RC$new() 279.6430
#> 2 R6$new() 49.8450
#> 3 R6NoClass$new() 44.8755
#> 4 R6NonPortable$new() 48.0375
#> 5 R6NonCloneable$new() 48.0385
#> 6 R6Bare$new() 39.1540
#> 7 R6Private$new() 67.6145
#> 8 R6PrivateBare$new() 60.5370
#> 9 FunctionEnvClass() 3.0120
#> 10 FunctionEnvNoClass() 1.5060
以下のプロットはインスタンス化にかかった時間の中央値を示しています.
参照クラスは,他の種類のクラスのインスタンス化よりずっと低速です.R6オブジェクトのインスタンス化は,おおよそ5倍高速です.単純な関数呼び出しで環境を作成するのは,さらに20~30倍高速です.
フィールドへのアクセス速度
オブジェクトのフィールドにアクセスするのには,どれだけ時間がかかるのでしょうか.まずオブジェクトを作ります.
rc <- RC$new()
r6 <- R6$new()
r6noclass <- R6NoClass$new()
r6noport <- R6NonPortable$new()
r6noclone <- R6NonCloneable$new()
r6bare <- R6Bare$new()
r6priv <- R6Private$new()
r6priv_bare <- R6PrivateBare$new()
fun_env <- FunctionEnvClass()
fun_env_nc <- FunctionEnvNoClass()
そして,これらのオブジェクトから値を取得します.
speed <- microbenchmark(
rc$x,
r6$x,
r6noclass$x,
r6noport$x,
r6noclone$x,
r6bare$x,
r6priv$x,
r6priv_bare$x,
fun_env$x,
fun_env_nc$x
)
#> Warning in microbenchmark(rc$x, r6$x, r6noclass$x, r6noport$x, r6noclone
#> $x, : Could not measure a positive execution time for 30 evaluations.
speed <- mb_summary(speed)
speed
#> name median
#> 1 rc$x 7.529
#> 2 r6$x 0.904
#> 3 r6noclass$x 0.000
#> 4 r6noport$x 0.904
#> 5 r6noclone$x 0.904
#> 6 r6bare$x 0.000
#> 7 r6priv$x 0.904
#> 8 r6priv_bare$x 0.000
#> 9 fun_env$x 0.904
#> 10 fun_env_nc$x 0.000
参照クラスのフィールドへのアクセスは,他の方法よりもずっと低速です.
また,環境(R6か関数呼び出しで作成したもの)のフィールドへのアクセスには,class
属性があると遅くなるという明確なパターンがあります.これは,class
属性を持つオブジェクトに対してRが$
のS3メソッドを探そうとするのが性能上の損失となるからです.これについては以下でさらに詳しく見ます.
フィールドの設定速度
オブジェクトのフィールドに値を設定するのにかかる時間はどれくらいでしょうか.
speed <- microbenchmark(
rc$x <- 4,
r6$x <- 4,
r6noclass$x <- 4,
r6noport$x <- 4,
r6noclone$x <- 4,
r6bare$x <- 4,
# r6priv$x <- 4, # プライベートフィールドは直接設定できないので,
# r6priv_nc_np$x <- 4, # この2つは省略する
fun_env$x <- 4,
fun_env_nc$x <- 4
)
speed <- mb_summary(speed)
speed
#> name median
#> 1 rc$x <- 4 38.5510
#> 2 r6$x <- 4 1.8070
#> 3 r6noclass$x <- 4 0.6030
#> 4 r6noport$x <- 4 1.8075
#> 5 r6noclone$x <- 4 1.9580
#> 6 r6bare$x <- 4 0.6030
#> 7 fun_env$x <- 4 1.8070
#> 8 fun_env_nc$x <- 4 0.6020
やはり参照クラスは他のものよりはるかに低速です.この場合には,値の型チェックによる追加のオーバーヘッドがあります.
ここでもclass
属性なしのオブジェクトはそうでないものよりはるかに高速です.これも`$<-`
関数のS3ディスパッチが試みられるのが原因でしょう.
フィールドにアクセスするメソッド呼び出しの速度
各オブジェクトのメソッドを呼び出すときのオーバーヘッドはどのくらいでしょうか.どのオブジェクトのgetx()
メソッドも単にx
の値を返すだけのものです.このメソッドは必要な場合(portable=TRUE
のR6オブジェクトの場合)にはself$x
を使います.他の場合(portable=FALSE
のR6と参照クラスの場合)には単にx
を使います.
speed <- microbenchmark(
rc$getx(),
r6$getx(),
r6noclass$getx(),
r6noport$getx(),
r6noclone$getx(),
r6bare$getx(),
r6priv$getx(),
r6priv_bare$getx(),
fun_env$getx(),
fun_env_nc$getx()
)
speed <- mb_summary(speed)
speed
#> name median
#> 1 rc$getx() 7.529
#> 2 r6$getx() 2.409
#> 3 r6noclass$getx() 0.302
#> 4 r6noport$getx() 1.205
#> 5 r6noclone$getx() 2.410
#> 6 r6bare$getx() 0.301
#> 7 r6priv$getx() 1.506
#> 8 r6priv_bare$getx() 0.301
#> 9 fun_env$getx() 1.204
#> 10 fun_env_nc$getx() 0.301
参照クラスが最も低速です.
r6
も他のものよりはいくらか低速です.
r6priv
はr6
と同じ速さだろうと思うかもしれませんが,r6priv
の方が高速です.r6priv
にはclass
属性があるのでr6priv$getx
は遅いのですが,private
にはclass
属性がないので,private$x
はself$x
より高速なのです.
x
に直接(self
やprivate
なしで)アクセスできて,class
属性もないオブジェクトが最速です.
self$x <-
とx <<-
による代入
参照クラスでは,<<-
演算子か.self
オブジェクトを用いてフィールドを変更できます.例として,以下の2つのクラスのsetx()
メソッドを比べてみてください.
RCself <- setRefClass("RCself",
fields = list(x = "numeric"),
methods = list(
initialize = function() .self$x <- 1,
setx = function(n = 2) .self$x <- n
)
)
RCnoself <- setRefClass("RCnoself",
fields = list(x = "numeric"),
methods = list(
initialize = function() x <<- 1,
setx = function(n = 2) x <<- n
)
)
移植不可R6クラスもこれと同様です.ただし.self
ではなくself
を使います.
R6self <- R6Class("R6self",
portable = FALSE,
public = list(
x = 1,
setx = function(n = 2) self$x <- n
)
)
R6noself <- R6Class("R6noself",
portable = FALSE,
public = list(
x = 1,
setx = function(n = 2) x <<- n
)
)
rc_self <- RCself$new()
rc_noself <- RCnoself$new()
r6_self <- R6self$new()
r6_noself <- R6noself$new()
speed <- microbenchmark(
rc_self$setx(),
rc_noself$setx(),
r6_self$setx(),
r6_noself$setx()
)
speed <- mb_summary(speed)
speed
#> name median
#> 1 rc_self$setx() 45.7790
#> 2 rc_noself$setx() 28.3105
#> 3 r6_self$setx() 4.8190
#> 4 r6_noself$setx() 2.1080
参照クラスと移植不可R6クラスの両方とも,.self$x <-
による代入はx <<-
よりやや低速です.
R6クラスはデフォルトでは移植可能なので,x <<-
での代入はできないということは覚えておいてください.
class
属性を持つオブジェクトにおける$
使用のオーバーヘッド
class
属性を持つオブジェクトで$
を使うときにはオーバーヘッドが生じます.以下のテストでは3つの異なる種類のオブジェクトを作成します.
-
class
属性を持たない環境 -
class
属性"e2"
を持つが,S3メソッド$.e2
は持たない環境 -
class
属性"e3"
を持ち,単にNULLを返すS3メソッド$.e3
を持つ環境
各環境はオブジェクトx
を含むものとします.
e1 <- new.env(hash = FALSE, parent = emptyenv())
e2 <- new.env(hash = FALSE, parent = emptyenv())
e3 <- new.env(hash = FALSE, parent = emptyenv())
e1$x <- 1
e2$x <- 1
e3$x <- 1
class(e2) <- "e2"
class(e3) <- "e3"
# クラスe3のS3メソッドを定義
`$.e3` <- function(x, name) {
NULL
}
これでそれぞれの種類のオブジェクトに対して,$
呼び出しの時間計測テストが実行できます.e3
オブジェクトの$
関数は何もせず,単にNULL
返すだけであることに注意してください.
speed <- microbenchmark(
e1$x,
e2$x,
e3$x
)
#> Warning in microbenchmark(e1$x, e2$x, e3$x): Could not measure a positive
#> execution time for 23 evaluations.
speed <- mb_summary(speed)
speed
#> name median
#> 1 e1$x 0.000
#> 2 e2$x 0.603
#> 3 e3$x 0.603
e2
とe3
で$
を使うと,e1
よりかなり低速です.これはe2
とe3
にはclass
属性があるからです.e2
には$
メソッドは定義されていないにもかかわらず,Rが適切なS3メソッドを探すせいで,e2$x
はe1$x
の約6倍も低速です.
e3$x
はe2$x
よりわずかに高速です.これはおそらく,$.e3
関数が実際にはNULL
を返す以外は何もしないからでしょう2.
オブジェクトがclass
属性を持つ場合,Rは$
が呼び出されるたびにメソッドの探索を試みます.$
が頻繁に使われる場合には,これによって大きく速度が低下することがあり得ます.
リストと環境,$
と[[
クラスを作成するのにはリストも使えます(参照セマンティクスではありませんが).リストと環境において,$
を用いて項目にアクセスするのにかかる時間はとれくらいでしょうか.また,obj$x
を使う場合とobj[['x']]
を使う場合も比較してみます.
lst <- list(x = 10)
env <- new.env()
env$x <- 10
mb_summary(microbenchmark(
lst = lst$x,
env = env$x,
lst[['x']],
env[['x']]
))
#> Warning in microbenchmark(lst = lst$x, env = env$x, lst[["x"]],
#> env[["x"]]): Could not measure a positive execution time for 101
#> evaluations.
#> name median
#> 1 lst 0
#> 2 env 0
#> 3 lst[["x"]] 0
#> 4 env[["x"]] 0
環境とリストの性能は同等です.
[[
演算子は$
よりわずかに高速です.おそらく[[
は未評価のシンボルを文字列に変換する必要がないからでしょう3.
まとめ
R6オブジェクトはRの参照クラスオブジェクトよりメモリ消費が少なく,はるかに高速です.また,R6にはさらに速度を向上させるオプションもあります.
以上のテストにおいて,R6クラスの速度を最も大きく向上させるのは,class
属性を使わないことです.これは$
を使うときの速度を向上させます.移植不可R6クラスも,フィールドに$
をまったく使わずにアクセスできるので,やや速度を向上させることができます.ほとんどの場合において,これらの高速化は無視できる程度(マイクロ秒のオーダー)であり,クラスメンバへのアクセスが何万回あるいは何十万回も行われるときにのみ重要になるでしょう.
付録
オブジェクトサイズを計算する関数
# サイズ計算用の便利関数
obj_size <- function(expr, .env = parent.frame()) {
size_n <- function(n = 1) {
objs <- lapply(1:n, function(x) eval(expr, .env))
as.numeric(do.call(object_size, objs))
}
data.frame(one = size_n(1), incremental = size_n(2) - size_n(1))
}
obj_sizes <- function(..., .env = parent.frame()) {
exprs <- as.list(match.call(expand.dots = FALSE)$...)
names(exprs) <- lapply(1:length(exprs),
FUN = function(n) {
name <- names(exprs)[n]
if (is.null(name) || name == "") paste(deparse(exprs[[n]]), collapse = " ")
else name
})
sizes <- mapply(obj_size, exprs, MoreArgs = list(.env = .env), SIMPLIFY = FALSE)
do.call(rbind, sizes)
}
システム情報 4
sessionInfo()
#> R version 3.3.2 (2016-10-31)
#> Platform: x86_64-w64-mingw32/x64 (64-bit)
#> Running under: Windows 10 x64 (build 14393)
#>
#> locale:
#> [1] LC_COLLATE=Japanese_Japan.932 LC_CTYPE=Japanese_Japan.932
#> [3] LC_MONETARY=Japanese_Japan.932 LC_NUMERIC=C
#> [5] LC_TIME=Japanese_Japan.932
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] scales_0.4.1 ggplot2_2.2.0.9000 R6_2.2.0
#> [4] pryr_0.1.2 microbenchmark_1.4-2.1
#>
#> loaded via a namespace (and not attached):
#> [1] Rcpp_0.12.8 knitr_1.15.1 magrittr_1.5 MASS_7.3-45
#> [5] splines_3.3.2 munsell_0.4.3 lattice_0.20-34 colorspace_1.3-1
#> [9] multcomp_1.4-6 stringr_1.1.0 plyr_1.8.4 tools_3.3.2
#> [13] grid_3.3.2 gtable_0.2.0 TH.data_1.0-7 htmltools_0.3.5
#> [17] survival_2.40-1 yaml_2.1.14 lazyeval_0.2.0 rprojroot_1.1
#> [21] digest_0.6.10 assertthat_0.1 tibble_1.2 Matrix_1.2-7.1
#> [25] codetools_0.2-15 evaluate_0.10 rmarkdown_1.2 labeling_0.3
#> [29] sandwich_2.3-4 stringi_1.1.2 backports_1.0.4 mvtnorm_1.0-5
#> [33] zoo_1.7-13