読者です 読者をやめる 読者になる 読者になる

メモ:switch()の代わりにS3 generic functionを使うのは微妙だったかも

去年こんな記事を書いたけど、

notchained.hatenablog.com

でもメソッドディスパッチのコストってどれくらいなんだろう?と思ってやってみました。

去年に引き続き、こういうデータがあるとする。

j <- jsonlite::fromJSON('{
  "日付": {
    "type": "date",
    "value": "2015-11-11"
  },
  "何の日": {
    "type": "char",
    "value": "ポッキーの日"
  },
  "俺の中での重要度": {
    "type": "int",
    "value": "3"
  }
}')

switch()を使うバージョンはこんな感じ。

library(purrr)

use_switch <- function(j) {
  map(j,
      ~ switch(.$type,
               date = as.Date(.$value),
               char = as.character(.$value),
               int  = as.integer(.$value)))
}

S3の総称関数に頼るやつはこれ。

parse_j <- function(x) UseMethod("parse_j")

parse_j.default <- function(x) as.character(x)
parse_j.date <- function(x) as.Date(as.character(x))
parse_j.char <- function(x) as.character(x)
parse_j.int  <- function(x) as.integer(x)


use_S3 <- function(j) {
  map(j, ~ structure(.$value, class = .$type)) %>%
    map(parse_j)
}

あと、purrr::when()を使うバージョンも試してみる。

use_when <- function(j) {
  map(j,
      ~ when(.,
             .$type == "date" ~ as.Date(.$value),
             .$type == "char" ~ as.character(.$value),
             .$type == "int" ~ as.integer(.$value)))
}

結果が同じであることをいちおう確認しておく。

testthat::expect_identical(use_S3(j), use_switch(j))
testthat::expect_identical(use_S3(j), use_when(j))

ベンチマークを取ってみる。

microbenchmark::microbenchmark(use_S3(j),
                               use_switch(j),
                               use_when(j))
#> Unit: microseconds
#>           expr     min       lq     mean   median       uq     max neval
#>      use_S3(j) 235.852 257.5810 283.7692 266.2720 293.7285 551.902   100
#>  use_switch(j)  95.605 105.0865 114.6075 111.2105 117.9260 222.420   100
#>    use_when(j) 226.765 246.5190 269.2388 257.1850 279.9015 417.580   100

データ数が増えると差は縮まるとはいえ、けっこう差がある感じがします。うーん。

j1000 <- sample(j, 1000, replace = TRUE)

microbenchmark::microbenchmark(use_S3(j1000),
                               use_switch(j1000),
                               use_when(j1000))
#> Unit: milliseconds
#>               expr      min       lq     mean   median       uq       max neval
#>      use_S3(j1000) 31.27074 34.26768 39.24168 37.22966 39.88467 140.86846   100
#>  use_switch(j1000) 18.64337 20.77967 23.29911 22.15409 24.56911  50.86542   100
#>    use_when(j1000) 61.54355 68.09684 72.94117 70.67956 74.64144 137.47645   100

ちなみに差はどうやらメソッドディスパッチではなくて、

  map(j, ~ structure(.$value, class = .$type)) %>%

の部分みたいです。まあそうですよねー。。

Advanced Rに出てくる例のように自分で生成するようなオブジェクトを扱うときはS3の総称関数が便利ですが、こういう外から与えられるデータ構造をパースするためだけにクラスを設定して使うというのはめんどくさそうです。

あと、前回は触れていませんでしたが、独自のS3クラスを設定してしまうので別のS3総称関数がそのままは使えなくなるのも難点です。例えば、

parse_j.date <- function(x) as.Date(as.character(x))

になぜas.character()がいるかというと、いったんxcharacterに戻さないとas.Date()のメソッドディスパッチの仕組みがうまく動かないからです。自分でクラスを設定して、それをまた戻して、というのはなんか遠回りしている感じがあります。こういうときは素直にswitch()を使っといたほうがよさそうです。