What's your clone policy? - Data::Clone

複雑なデータ構造のコピーにはStorable::dclone()やClone::clone()がよく使われてきた。しかし,これらのクローンポリシーには疑問がある。そこで,新しいクローンモジュールを書いてみた。

さて,まずStorableのポリシーはおおむね以下の通り:

  • リファレンスはすべて深いコピー(deep copy)
  • スカラー値に対しては,Perlレベルでの代入に等しい操作を行い,マジックは無視する
  • オブジェクトに対しては,フックが定義されていればそれを使い,未定義であればその他のリファレンスと同じように扱う

次に,Cloneのポリシーはおおむね以下の通り:

  • リファレンスはすべて深いコピー
  • スカラー値に対しては,マジックも含めて可能な限りコピーする
  • オブジェクトもその他のリファレンスと同じように扱う。この挙動を変えることはできない

これらのモジュールは「オブジェクトに対してはデフォルトで深いコピーを行う」というクローンポリシーを持っているが,これがよくない。
たとえば,ある外部リソースを管理するHandleオブジェクトがあるとする。Storableでは,HandleクラスにStorable用のフックがあればdclone($handle)を行っても安全だが,そうでなければ単にデータ構造がコピーされ,その外部リソースの意図しない解放が起きる可能性がある。一方Cloneによるclone($handle)では,意図しない挙動を引き起こす可能性がStorableより高い。つまり,Storable/Cloneでコピーを行う場合は,コピーすべきデータが何かを把握していなければならない。

そこで,Data::Cloneでは,オブジェクトに対してはデフォルトで浅いコピー(surface copy)を行うことにする。そして,オブジェクトがcloneメソッドを持っていれば,そのcloneメソッドを呼び出す。オブジェクトのcloneメソッドで単に深いコピーを行いたいだけなら,clone()関数をインポートすればよい。

package MyNoclonable; # デフォルトは深いコピーを行わない
# ...
package MyClonable; # 深いコピーを行いたいとき
use Data::Clone; # MyClonableは深いコピーが行われる
# ...
package MyCustomClonable; # 独自のコピーを行いたいとき
use Data::Clone qw(data_clone);
sub clone {
    my($self) = @_;
    # まずデータ構造全体をコピーして…
    my $newobj = data_clone($self);
    # その後外部リソースをセットアップ
    $newobj->{handle} = $self->_clone_handle();
    return $newobj;
}

もっとも,これはこれで完全ではなく,対象のオブジェクトがクローン可能だがそのためのメソッドがcloneではないとき,意図せず浅いコピーが行われてしまう。つまりこれは,Storable::dclone()やClone::clone()を置き換えるというより,必要となるポリシーに合わせて選択するべきモジュールだということだ。

なお,これは当初単にクローンポリシーが異なるだけのつもりだったが,実際に実装してみたところ非常に高速になった。

Data-Clone $ perl -Mblib benchmark/vs_clone.pl
ArrayRef:
                Rate    Storable       Clone Data::Clone
Storable     29537/s          --        -58%        -89%
Clone        71087/s        141%          --        -74%
Data::Clone 270490/s        816%        281%          --
HashRef:
                Rate    Storable       Clone Data::Clone
Storable     30632/s          --        -53%        -86%
Clone        65163/s        113%          --        -70%
Data::Clone 220553/s        620%        238%          --

配列リファレンスやハッシュリファレンスに対する挙動はみな同じなので,なぜここまで違うのかは不可解だが,とにかくこのような結果となった。