web-dev-qa-db-ja.com

Rのcopy-on-modifyセマンティクスとは正確には何であり、正規のソースはどこにありますか?

たまに、私はRがcopy-on-modify semanticsの概念に出くわします。たとえば Hadley's devtools wiki =。

ほとんどのRオブジェクトにはコピーオンモディファイのセマンティクスがあるため、関数の引数を変更しても元の値は変更されません

この用語は、R-Helpメーリングリストにさかのぼることができます。たとえば、Peter Dalgaardは次のように書いています 2003年7月

Rは関数型言語であり、遅延評価と弱い動的型付けが使用されます(変数は型を自由に変更できます:a <-1; a <-"a"が許可されます)。意味的には、すべてがコピーオンモディファイですが、最悪の非効率を回避するために実装ではいくつかの最適化トリックが使用されています。

同様に、Peter Dalgaardは 2004年1月 に次のように書いています。

Rには変更時のコピーのセマンティクス(原則として、実際には時々)があるため、オブジェクトの一部が変更されると、オブジェクト自体を含む、それを含んでいたものを新しい場所で探す必要がある場合があります。

さらに戻ると、 2000年2月 で、Ross Ihakaは次のように述べています。

私たちはこれを実現するためにかなりの労力を費やしました。セマンティクスは「変更時にコピー(必要な場合)」と説明します。コピーは、オブジェクトが変更されたときにのみ行われます。 (必要な場合)の部分は、変更によって非ローカル変数を変更できないことを証明できる場合は、コピーせずに先に進んで変更することを意味します。

マニュアルにはありません

どんなに検索しても、- Rマニュアル にも R言語定義 にも、 "-on-modify"への参照も見つかりません。 R内部

質問

私の質問には2つの部分があります。

  1. これはどこに正式に文書化されていますか?
  2. コピーオンモディファイはどのように機能しますか?

たとえば、promiseが関数に渡されるので、「参照渡し」について話すのは適切ですか?

73
Andrie

値渡し

R言語定義 はこれを示します(セクション 4.3.3引数評価

R引数で関数を呼び出すセマンティクスは、call-by-valueです。一般に、指定された引数は、指定された値と対応する仮引数の名前で初期化されたローカル変数であるかのように動作します。 関数内で指定された引数の値を変更しても、呼び出しフレームの変数の値には影響しません。 【エンファシス追加】

これは、copy-on-modifyが機能するメカニズムを説明していませんが、関数に渡されたオブジェクトを変更しても、呼び出しフレームのオリジナル。

特にcopy-on-modifyアスペクトに関する追加情報は、 R InternalsのSEXPsの説明に記載されています。マニュアル 、セクション 1.1.2ヘッダーの残り 。具体的には[エンファシスを追加]

namedフィールドは、SET_NAMEDおよびNAMEDマクロによって設定およびアクセスされ、値01および2を取ります。 Rには 'call by value'イリュージョンがあるため、次のような割り当て

b <- a

aのコピーを作成し、bとして参照するように見えます。ただし、abも後で変更されない場合、コピーする必要はありません。実際に発生するのは、新しいシンボルbaと同じ値にバインドされ、値オブジェクトのnamedフィールドが設定されます(この場合は2)。オブジェクトが変更される直前に、namedフィールドが参照されます。 2の値は、オブジェクトを変更する前に複製する必要があることを意味します。 (これは複製する必要があることを示しているのではなく、必要であるかどうかにかかわらず複製する必要があることを示していることに注意してください。)0の値は、他のSEXPがデータを共有していないことがわかっていることを意味しますこのオブジェクトを使用すると、安全に変更できます。 1の値は、次のような状況で使用されます

dim(a) <- c(7, 2)

原則として、計算中にaの2つのコピーが存在します。

a <- `dim<-`(a, c(7, 2))

しかし、もはやではなく、そのため、一部のプリミティブ関数を最適化して、この場合のコピーを回避できます。

これはオブジェクトが引数として関数に渡される状況については説明していませんが、特に前に引用したR言語の定義からの情報を考えると、同じプロセスが動作していると推測できます。

機能評価の約束

約束関数に渡されたであると言うのは正確ではないと私は思います。引数は関数に渡され、使用される実際の式はプロミス(および呼び出し環境へのポインター)として格納されます。 forcingと呼ばれるプロセスである、ポインターが示す環境内で、引数が評価されたときのみ、promiseに格納された式が取得および評価されます。

そのため、この点に関してpass-by-referenceについて話すのは正しいとは思えません。 Rにはcall-by-valueセマンティクスがありますが、引数に渡された値が評価および変更されない限り、コピーを回避しようとします。

NAMEDメカニズムは、Rが変更時にコピーを作成する必要があるかどうかを追跡できるようにする最適化です(コメントの@hadleyで示されています)。 Peter Dalgaardによって議論されているように、NAMEDメカニズムが正確にどのように動作するかには、いくつかの微妙な点があります(質問へのコメントで R Develスレッド @mnelが引用しています)

42
Gavin Simpson

私はいくつかの実験を行いましたが、Rは常に最初の変更でオブジェクトをコピーすることを発見しました。

私のマシンで結果を見ることができます http://rpubs.com/wush978/5916

間違いがあったら教えてください、ありがとう。


オブジェクトがコピーされたかどうかをテストするには

次のCコードでメモリアドレスをダンプします。

#define USE_RINTERNALS
#include <R.h>
#include <Rdefines.h>

SEXP dump_address(SEXP src) {
  Rprintf("%16p %16p %d\n", &(src->u), INTEGER(src), INTEGER(src) - (int*)&(src->u));
  return R_NilValue;
}

2つの住所が出力されます。

  • SEXPのデータブロックのアドレス
  • integerの連続ブロックのアドレス

このC関数をコンパイルしてロードしてみましょう。

Rcpp:::SHLIB("dump_address.c")
dyn.load("dump_address.so")

セッション情報

これがテスト環境のsessionInfoです。

sessionInfo()

書き込み時にコピー

まず、copy on writeのプロパティをテストします。つまり、Rはオブジェクトが変更されたときにのみオブジェクトをコピーします。

a <- 1L
b <- a
invisible(.Call("dump_address", a))
invisible(.Call("dump_address", b))
b <- b + 1
invisible(.Call("dump_address", b))

オブジェクトbは、変更時にaからコピーします。 Rはcopy on writeプロパティ。

その場でベクトル/マトリックスを修正する

次に、ベクトル/行列の要素を変更するときに、Rがオブジェクトをコピーするかどうかをテストします。

長さ1のベクトル

a <- 1L
invisible(.Call("dump_address", a))
a <- 1L
invisible(.Call("dump_address", a))
a[1] <- 1L
invisible(.Call("dump_address", a))
a <- 2L 
invisible(.Call("dump_address", a))

アドレスは毎回変更されます。つまり、Rはメモリを再利用しません。

長いベクトル

system.time(a <- rep(1L, 10^7))
invisible(.Call("dump_address", a))
system.time(a[1] <- 1L)
invisible(.Call("dump_address", a))
system.time(a[1] <- 1L)
invisible(.Call("dump_address", a))
system.time(a[1] <- 2L)
invisible(.Call("dump_address", a))

長いベクトルの場合、Rは最初の変更後にメモリを再利用します。

さらに、上記の例は、オブジェクトが巨大な場合、「インプレイス変更」がパフォーマンスに影響することも示しています。

マトリックス

system.time(a <- matrix(0L, 3162, 3162))
invisible(.Call("dump_address", a))
system.time(a[1,1] <- 0L)
invisible(.Call("dump_address", a))
system.time(a[1,1] <- 1L)
invisible(.Call("dump_address", a))
system.time(a[1] <- 2L)
invisible(.Call("dump_address", a))
system.time(a[1] <- 2L)
invisible(.Call("dump_address", a))

Rは最初の変更時にのみオブジェクトをコピーするようです。

理由はわかりません。

属性の変更

system.time(a <- vector("integer", 10^2))
invisible(.Call("dump_address", a))
system.time(names(a) <- paste(1:(10^2)))
invisible(.Call("dump_address", a))
system.time(names(a) <- paste(1:(10^2)))
invisible(.Call("dump_address", a))
system.time(names(a) <- paste(1:(10^2) + 1))
invisible(.Call("dump_address", a))

結果は同じです。 Rは、最初の変更時にのみオブジェクトをコピーします。

26
wush978