Advanced R: Names and values
はじめに
Advanced R 2nd EditionのPaperBook版が届いたので、Rについてのおさらい備忘録。また、書籍を読んでいるにも関わらず誤った解釈や用語の誤用もあるかもしれませんので、参考にする際は自己責任でお願いします。
この記事のライセンスはAdvanced R 2nd Editionと同様で下記の通りです。
Names and values
Rの名前と値の関係について書いてある章。何気なくx <- 1
とかしているけれども、どういった仕組みが裏側に用意されているのかを図解を交えて解説してある。
Binding
名前と値を関連付けることをバインディングという。Advanced R 2nd Editionの例をお借りする。これを日本語的に解釈すると、c(1, 2, 3)
というベクトルのオブジェクトを名前x
がバインドしている、ということになる。
x <- c(1, 2, 3)
x
を使ってy
に、y
を使ってz
に、z
を使ってxx
で…としても、Rはc(1, 2, 3)
というベクトルのオブジェクトを名前x
とy
とz
とxx
の各々がバインドすることになる。そのため、オブジェクトの識別子にアクセスしても0x7f886b0ad6d0
のままとなる。
x <- 1:3 obj_addr(x) [1] "0x7f886b0ad6d0" y <- x obj_addr(y) [1] "0x7f886b0ad6d0" z <- y obj_addr(z) [1] "0x7f886b0ad6d0" xx <- z obj_addr(xx) [1] "0x7f886b0ad6d0"
同じc(1, 2, 3)
というベクトルのオブジェクトを作って、y
でバインディングしても、先ほどのオブジェクトとは異なることがわかる。
y <- 1:3 obj_addr(y) [1] "0x7f8873008e48"
値渡し、参照渡しみたいなものがあるが、下記のようにオブジェクトを入れると同じアドレスをオブジェクトたちは参照する。
x <- c(1, 2, 3) y <- x obj_addr(x);obj_addr(y) [1] "0x7f8aefc93468" [1] "0x7f8aefc93468"
この状態で、x
に新たな値を付け替えると、その時点でコピーが発生し、違うメモリが利用される。Copy-on-modifyという仕組み。これは後述。
obj_addr(x);obj_addr(y) [1] "0x7f8aee31b218" [1] "0x7f8aefc93468" identical(obj_addr(x), obj_addr(y)) [1] FALSE
ちなみに関数のメモリも調べることができる。
lobstr::obj_addr(mean) [1] "0x7f8ae9caca70" lobstr::obj_addr(base::mean) [1] "0x7f8ae9caca70" lobstr::obj_addr(match.fun("mean")) [1] "0x7f8ae9caca70"
補足として、match.fun
はうっかり関数を上書きしても、正しく呼び出すことができる関数。
is.function(outer) [1] TRUE outer <- 1:5 is.function(outer) [1] FALSE is.function(match.fun(outer)) [1] TRUE wrap_func <- function(search_lib, ...){ func <- match.fun(search_lib) res <- func(...) return(res) } wrap_func(rnorm, n = 10, mean = 0, sd = 1) [1] -0.3497536 -0.1834132 1.2715276 -1.6678541 -0.6154758 -1.0373960 [7] 0.0120612 0.1599219 0.4485161 -1.5320582
Non-syntactic names
Non-syntactic namesという非構文名のことについて。基本的に予約語や_
や数字などを名前の先頭に持ってきて、バインディングすることはできない。
TRUE <- 1 TRUE <- 1 でエラー: 代入の左辺が不正 (do_set) です _x <- 1 エラー: 想定外の入力です in "_" 1x <- 1 エラー: 想定外のシンボルです in "1x"
ただし、バッククォートで囲むことでバインディングできるが、非推奨。
`TRUE` <- 1 `_x` <- 1 `1x` <- 1 TRUE [1] TRUE `TRUE` [1] 1 `_x` [1] 1 `1x` [1] 1
Copy-on-modify
Copy-on-modifyというオブジェクトのコピーについて。オブジェクトはいつコピーされるのかを解説している。さきほど、オブジェクトをx
でバインディングした際に異なる名前でもバインディングしたが、同じオブジェクトを参照していた。下記のような場合は、オブジェクトの識別子が0x7f88724d6348
→0x7f8872563b18
と異なっている。
これがCopy-on-modifyという仕組みで、元のオブジェクトに変更が生じる際に、元のオブジェクトをコピーして新たなオブジェクトをリバインディングすることになる。つまり、コピーしてモディファイするので、Copy-on-modifyなのである。
x <- c(1, 2, 3) obj_addr(x) [1] "0x7f88724d6348" x[[3]] <- 4 obj_addr(x) [1] "0x7f8872563b18"
tracemem()
を使うことで、いつコピーが発生したのかを確認できる。x
のメモリアドレスは0x7f8873be93b8
だったが、値の一部を変更したことで、Copy-on-modifyが発生し、メモリアドレスが0x7f8873be93b8 -> 0x7f8873c5e918
に変更されている。
x <- c(1, 2, 3) cat(tracemem(x)) <0x7f8873be93b8> x[[3]] <- 4 tracemem[0x7f8873be93b8 -> 0x7f8873c5e918]: cat(tracemem(x)) <0x7f8873c5e918>
このCopy-on-modifyはオブジェクトだけではなく関数にも同様に適用される。
x <- 1:3 f <- function(a) { print(a) } cat(tracemem(f(x))) [1] 1 2 3 <0x7f886fd1cc38> g <- f(x) [1] 1 2 3
この仕組は少しややこしいので、Advanced R 2nd Editionのイメージ図をお借りする。
f()
は右側の黄色いオブジェクトで、形式的に引数a
を持つ。f()
を実行すると、形式的に引数a
はx
がバインディングしているオブジェクトをバインディングする(このときもバインディングという表現が正しいのか不明)。このとき、関数が実行されると、実行環境(灰色の箱)でf()
をバインディングすることになる。z
はx
がバインディングしているオブジェクトをバインディングするだけなので、Copy-on-modifyは発生しない。
Lists
リストのバインディングについて。リストのバインディングは先ほどよりもいささかややこしい。例えば書籍に載っているこんな例があるとする。
l1 <- list(1, 2, 3) > l1 [[1]] [1] 1 [[2]] [1] 2 [[3]] [1] 3
Advanced R 2nd Editionのイメージ図をお借りする。図からわかるように、リストは、値を格納するのではなく、値への参照を格納する。
Copy-on-modifyについては、同じ参照のままであれば変わらない。参照が変わるような修正を行った場合、Copy-on-modifyが発生し、0x7f8872ce2468 -> 0x7f887269ed08
へとメモリアドレスが変更される。
l1 <- list(1, 2, 3) cat(tracemem(l1)) <0x7f8872ce2468> l2 <- l1 l2[[3]] <- 4 tracemem[0x7f8872ce2468 -> 0x7f887269ed08]:
さらに詳しく参照関係を見たい場合はlobstr::ref()
を使用する。コメントアウトの記号でメモリアドレスを対応させている。
ref(l1, l2) █ [1:0x7f8872ce2468] <list> ├─[2:0x7f88716f38e8] <dbl> # ▲ ├─[3:0x7f88716f38b0] <dbl> # ■ └─[4:0x7f88716f3878] <dbl> # ● █ [5:0x7f887269ed08] <list> ├─[2:0x7f88716f38e8] # ▲ ├─[3:0x7f88716f38b0] # ■ └─[6:0x7f88716624e8] <dbl> # ×
リストの集合はデータフレームなので、データフレームも参照を格納する。
d1 <- data.frame(x = c(1, 5, 6), y = c(2, 4, 3))
このとき、列を変更すると、Copy-on-modifyが発生し、0x7f8874884058
への参照が0x7f8874a02d98
へと変わる。
d2 <- d1 d2[, 2] <- d2[, 2] * 2 ref(d1, d2) █ [1:0x7f8874229208] <df[,2]> ├─x = [2:0x7f88748840a8] <dbl> └─y = [3:0x7f8874884058] <dbl> █ [4:0x7f88744bb048] <df[,2]> ├─x = [2:0x7f88748840a8] └─y = [5:0x7f8874a02d98] <dbl>
行を変更すると、すべての列が変更されるので、すべての列でCopy-on-modifyが発生することになる。
d3 <- d1 d3[1, ] <- d3[1, ] * 3 ref(d1, d3) █ [1:0x7f8874229208] <df[,2]> ├─x = [2:0x7f88748840a8] <dbl> └─y = [3:0x7f8874884058] <dbl> █ [4:0x7f88749d1748] <df[,2]> ├─x = [5:0x7f8874c20898] <dbl> └─y = [6:0x7f8874c20848] <dbl>
文字ベクトル
文字ベクトルはグローバルストリングプールを使用する。なので、x
は文字ベクトルを下記のようにバインディングしている。そして、各値はグローバルストリングプールを参照します。
x <- c("a", "a", "abc", "d") ref(x, character = TRUE) █ [1:0x7f8871a41fa8] <chr> ├─[2:0x7f886b0a2320] <string: "a"> ├─[2:0x7f886b0a2320] ├─[3:0x7f8870e724c8] <string: "abc"> └─[4:0x7f886ca8a688] <string: "d">
for-loopの改良
Rはよく計算速度が遅いなどと言われる。ALTREPプロジェクトで大きく改善したらしいとも聞く。とにかく遅くなる要因の1つがCopy-on-modifyを発生させるコードが要因。下記のような計算をすると、ループする度に3回のCopy-on-modifyが発生する。
x <- data.frame(matrix(runif(5 * 10), ncol = 5)) medians <- vapply(x, median, numeric(1)) cat(tracemem(x), "\n") <0x7f8871721ad8> for (i in seq_along(medians)) { x[[i]] <- x[[i]] - medians[[i]] } # Loop-1 tracemem[0x7f8871721ad8 -> 0x7f886d64e758]: tracemem[0x7f886d64e758 -> 0x7f886d64e838]: [[<-.data.frame [[<- tracemem[0x7f886d64e838 -> 0x7f886d64e8a8]: [[<-.data.frame [[<- # Loop-2 tracemem[0x7f886d64e8a8 -> 0x7f886d64ead8]: tracemem[0x7f886d64ead8 -> 0x7f886d64eb48]: [[<-.data.frame [[<- tracemem[0x7f886d64eb48 -> 0x7f886d64ebb8]: [[<-.data.frame [[<- # Loop-3 tracemem[0x7f886d64ebb8 -> 0x7f886d64ec28]: tracemem[0x7f886d64ec28 -> 0x7f886d64f248]: [[<-.data.frame [[<- tracemem[0x7f886d64f248 -> 0x7f886d6836d8]: [[<-.data.frame [[<- # Loop-4 tracemem[0x7f886d6836d8 -> 0x7f8870d7ef78]: tracemem[0x7f8870d7ef78 -> 0x7f8870d7efe8]: [[<-.data.frame [[<- tracemem[0x7f8870d7efe8 -> 0x7f8870d7f058]: [[<-.data.frame [[<- # Loop-5 tracemem[0x7f8870d7f058 -> 0x7f8870d7f0c8]: tracemem[0x7f8870d7f0c8 -> 0x7f8870d7fc98]: [[<-.data.frame [[<- tracemem[0x7f8870d7fc98 -> 0x7f8870d7fd08]: [[<-.data.frame [[<-
データフレームの代わりにリストを使用すれば、リストを修正することはR内部ではCコードを使うので、参照はインクリメントされず、1つのコピーだけが作られる。なので、先ほどよりも高速になる。
y <- as.list(x) cat(tracemem(y), "\n") <0x7f887354aa18> for (i in 1:5) { y[[i]] <- y[[i]] - medians[[i]] } tracemem[0x7f887354aa18 -> 0x7f8873589c18]:
たしか…Data Sience for RのFunctionのところでは、基本的に最初にリストなりのアウトプットを格納する箱を用意してループさせることを推奨してた気がする(曖昧で…すいません)。
library(tidyverse) x <- data.frame(matrix(runif(5 * 10), ncol = 5)) medians <- vapply(x, median, numeric(1)) out <- vector(mode = "list", length = ncol(x)) for (i in seq_along(x)) { out[[i]] <- x[[i]] - medians[[i]] } y <- out %>% map_dfc(., c) y # A tibble: 10 x 5 V1 V2 V3 V4 V5 <dbl> <dbl> <dbl> <dbl> <dbl> 1 0.123 -0.0514 0.572 0.303 0.0858 2 -0.0968 0.207 -0.323 0.00796 0.144 3 0.0159 -0.257 0.518 -0.0643 -0.529 4 -0.0159 -0.314 -0.0670 -0.00796 -0.415 5 -0.344 0.0988 -0.0160 0.788 -0.0858 6 0.218 -0.340 0.0260 0.0779 0.320 7 0.503 -0.0644 0.0160 -0.200 0.194 8 0.183 0.562 -0.205 -0.0939 0.238 9 -0.125 0.0698 -0.199 0.436 -0.376 10 -0.171 0.0514 0.157 -0.106 -0.232