Rにおける値渡しと参照渡し (2)

昨日の記事を書いたあと、twitter で tracemem 使うといいよ、と教えていただきました (@sfchaosさん、ありがとうございました!)。

この関数ははじめて知ったのですが、ヘルプを見て意訳すると以下の様な感じです。

tracemem(x) を実行すると x について duplicate (複製)が生じた時にメッセージを表示する。これは2つのオブジェクトがメモリを共有している場合において、1つが変更された時に生じる。ちなみに untracemem(x) でメッセージフックを解除できる。

ということで関数の内部でコピーが起きたか、を判別するのにピッタリです。

今回の内容:

  • tracemem を使って値渡し、参照渡しを確かめる
  • 参照渡しなのか、値渡しなのか、が確定するタイミングについて考える

tracemem を使った方法

以下の方法は @sfchaos さんに教えてもらった方法そのままです。前回同様、prod1 は行列 A について値渡しになりそうな関数で、prod2 は参照渡しになりそうな関数です。

prod1 <- function(A,x){
  A <- A + diag(x)
  A %*% x
}

prod2 <- function(A,x){
  A %*% x
}

N <- 1000
A <- matrix(rnorm(N*N),nc=N)
x <- rnorm(N)

tracemem(A)
# => [1] "<0x7e790008>"
invisible(prod1(A,x))  # invisible は結果を print しないようにする関数
# => tracemem[0x7e790008 -> 0x7dfe0008]: prod1  # コピーが発生した!

invisible(prod2(A,x))
# => 何も表示されない!=コピーが生じていない!

ということで、前回は実行時間から推論しただけでしたが、やはり引数を変更するとコピーが生じる、ということで間違いないことが確認できました。

複製はどのタイミングで生じるのか?

前回、以下のように書きました。

そして、引数が変更されるかされないかはパースした段階でわかる(なのでパースの段階で値渡しか参照渡しかを判別することが可能)

ですが、これは誤りでした。実際、以下のような関数はパースの段階で値渡しか参照渡しかを判別できるでしょうか?

prod3 <- function(A,x,add=T){
  if(add) A <- A + diag(x)
  A %*% x
}

add が真のときは引数が変更されて、偽のときは引数が変更されません。

これをパースの時点で判別しようとすると、Rの副作用を作らないという原則から add がいかなる値であろうともコピーを行うことになるはずです。

もう一つの可能性としては、実際に変更が起きたその瞬間にコピーを作る、というものがありえるでしょう。

実験してみます。

N <- 1000
A <- matrix(rnorm(N*N),nc=N)
x <- rnorm(N)

tracemem(A)
# => [1] "<0x7dfe0008>"

invisible(prod3(A,x,add=T))
# => tracemem[0x7dfe0008 -> 0x7ef40008]: prod3

invisible(prod3(A,x,add=F))
# => 何も表示されない!=コピーが生じていない!

同じ関数でも引数の状態によってコピーが発生したり、しなかったり。add=F のときは prod3 の if の内部まで進まないため、A が変更されず、したがってコピーが発動しない、ということになります。

つまりRは

  • 引数のコピーを作るか(値渡しか)、作らないか(参照渡しか)は実際に引数が変更される瞬間ギリギリまで判別しない、
  • 変更される瞬間(直前?)で重い腰を上げてコピーを作成する(遅延評価)

という挙動をしているようですね。

まとめ

  • Rは基本的には値渡しであり、C++のように引数として与えられた変数を変更することで外側の世界に影響させることはできない
  • 関数内部で引数を変更しない場合は、コピーが生じない (C++ の const& のようなイメージ)
  • 関数内部に引数を変更するコードがあったとしても、実際に引数が変更される段まではコピーは生じない (遅延評価)

Rにおける値渡しと参照渡し

Rの関数に引数を渡すと値渡しになる、とずっと信じていたわけだけど、どうも違うらしい。Rはどうやら「自動的に」値渡しすべきか、参照渡しにすべきか、を判断しているようだ。

C++みたいな言語では「フツーに」引数を渡すと全部値渡しになって(特に行列のような巨大なオブジェクトを渡す場合には)効率が悪いので、ポインタや参照で渡したり、副作用を気にする場合は const 参照で渡したりする。

さて、上で述べた R の自動判断機能は以下のような事実に基づくようだ。

  • そもそも値渡しは関数に引数を渡した段階でオブジェクトのコピーを生成して「引数として渡された変数を関数の内部で変更してもスコープの外では値が変更されない」ことを保証するのだが、
  • オブジェクトが関数の内部で変更されないことが保証されるならば「関数の実行中はスコープの外でも値が変更されない」ことは保証されるのでコピーをそもそも生成する必要がない、
  • そして、引数が変更されるかされないかはパースした段階でわかる(なのでパースの段階で値渡しか参照渡しかを判別することが可能) → 間違いでした。詳細はRにおける値渡しと参照渡し(2)に書きました。

実験

これは以下のようにして確かめることができる、はず。

prod1 <- function(A,x){
  A[1,1] <- A[1,1] + 1
  A %*% x
}

prod2 <- function(A,x){
  x[1] <- x[1] + 1
  A %*% x
}
  • prod1, prod2 はともに引数として渡された行列 A とベクトル x の積を計算する
  • prod1 では A[1,1] に 1 を加える
  • prod2 では x[1] に 1 を加える(これはprod1の代入作業のコストと揃えるため)
  • その後、積を計算する

ということをやっているのだが、上で述べたようなことがただしければ、

  • prod1 では行列 A のコピーが発生し
  • prod2 ではベクトル x のコピーが発生する

ため、より巨大なオブジェクトを渡すことになる prod1 が (生じる四則演算の数は同一にもかかわらず) 大幅に遅くなることが予測される。

実際、1000 x 1000 程度の行列を考えると以下の様な結果が得られる。


N <- 1000
A <- matrix(rnorm(N*N),nc=N)
x <- rnorm(N)

system.time( for(i in 1:1000) prod1(A,x) )
# =>   user  system elapsed
# =>  11.03    3.50   14.56

system.time( for(i in 1:1000) prod2(A,x) )
# =>  user  system elapsed
# =>  4.59    0.01    4.62

prod1のほうがかなり遅くなっていることが分かる。これより、上で述べたことはおそらく正しいだろうと結論される。

まとめ

このことを知ったからといって高速なプログラムが書けるようになるわけではないが、少なくとも「こんな大きなオブジェクトを関数に渡すのは気が引ける(なのでグローバル変数にしてしまえ)」という不安を払拭することができると思う。

【追記】続きを書きました。

[c++11] ネストしたクラスでの decltype

c++11 の decltype について。入れ子クラスの場合のちょっとした問題。下のサンプルコードの一番最後の行でコンパイルエラーが出る。

struct Outer {
  int x;
  struct Inner {
    int y;
  };
};


int main(){
  Outer outer;  // OK
  Outer::Inner inner;  // OK

  decltype(outer) outer2;  // Valid for c++11
  decltype(inner) inner2;  // Valid for c++11

  decltype(outer)::Inner inner3; // Error!!!!
}

コンパイル。

$ g++ decltype_test.cpp --std=c++0x
decltype_test.cpp: In function ‘int main()’:
decltype_test.cpp:16: error: expected initializer before ‘inner3’

decltype(outer) は Outer と展開されるので decltype(outer)::Inner は Outer::Inner と展開されて欲しいところだけど、できないみたいですね。g++ のバージョンは 4.4.3 と 4.5.2 でためしてみました。

ほとんどのケースでは auto を併用すれば問題は解決するのだけど、たまに困ります。これは仕様なのか、未実装なのかはわかりませんが。

文献管理アプリについてのメモ

ここ数日間、肥大したコンピュータ内部の文献管理について少し調査&トライアルしてみました。

とりあえず Mendeley と zotero standalone を使ってみた。できるとよさげなことたち。

  • タグによる管理
  • 全文検索
  • 軽快な動作
  • 電子ブックリーダーのサポート
  • bibtex の書き出し
  • 論文サイト(Web of science など)と連携

“文献管理アプリについてのメモ”の続きを読む