去年こんな記事を書いたけど、
でもメソッドディスパッチのコストってどれくらいなんだろう?と思ってやってみました。
去年に引き続き、こういうデータがあるとする。
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()
がいるかというと、いったんx
をcharacter
に戻さないとas.Date()
のメソッドディスパッチの仕組みがうまく動かないからです。自分でクラスを設定して、それをまた戻して、というのはなんか遠回りしている感じがあります。こういうときは素直にswitch()
を使っといたほうがよさそうです。