Scalar::Util の weaken()
Shibuya.pm #9 での id:lestrrat さんの発表 での質疑応答において
- id:dankogai 氏
- weak references の実装はどのようになっていますか?
- id:lestrrat 氏
- あー準備してくるの忘れました。Scalar::Util の Util.xs を見てください :)
というやりとりがありました。その時*1は
weak reference の参照先の
REFCNT
を decrement している「だけ」じゃないの?
と(あさはかにも)思ったんですが,実装を調べてみました。実際には「それだけ」ではありませんでした,と。
以下 REFCNT
とか MAGIC
とかでてくるんで,前提条件として Shibuya.pm #9 での :lestrrat さんの発表(Perl 5 internals の世界にようこそ - daisuke maki)必聴です。
[http://search.cpan.org/perldoc?Scalar::Util:title=Scalar::Util]::weaken()
とは
リファレンスの循環参照によるメモリリークを Scalar::Util::weaken で解決する - naoyaのはてなダイアリー を見てください ;P
一応,軽く説明します。循環参照や自己参照があると,リファレンスカウンタ REFCNT
が自律的に 0 にならなくなってしまい,オブジェクトが解放されません。
{ my ( $a, $b, $c ); $a = \$b; # $a ==> $b $b = \$c; # $b ==> $c $c = \$a; # $c ==> $a # REFCNT of $a, $b, $c => 2 (by scope, and, by other variable) } # -- $_->{REFCNT} for ($a, $b, $c); # REFCNT of $a, $b, $c => 1 (by other variable) # they are not accessible lexically, but are not destroyed (entities still exist) # they will be released in "global destruction" phase
こんな感じで三すくみに実体が残ってしまい,そのまま実行が継続しているとメモリリークとなります。
[http://search.cpan.org/perldoc?Scalar::Util:title=Scalar::Util]::weaken()
を使うと,指定したリファレンスを「弱参照(weak reference)」にすることができます。
use Scalar::Util qw( weaken ); { my ( $a, $b, $c ); $a = \$b; # $a ==> $b $b = \$c; # $b ==> $c $c = \$a; # $c ==> $a weaken( $a ); # $a ---> $b, $b ===> $c, $c ===> $a # $a->{REFCNT} => 2 # $b->{REFCNT} => 1 # $c->{REFCNT} => 2 } # -- $_->{REFCNT} for ($a, $b, $c); # successfully destroyed
つまり,weaken()
で作られる weak reference の意味は,
わたしはあなたのことを陰から見つめているけど,あなたは好きなように生きて。
という感じになるでしょうか(うう気持ち悪い)。
実装を見てみる
Scalar::Util の Util.xs の weaken()
では sv_rvweaken()
を呼んでいるだけです。sv_rvweaken()
については perlapi に説明があります。
http://perldoc.perl.org/perlapi.html#SV-Body-Allocationsv_rvweaken
Weaken a reference: set the
SvWEAKREF
flag on this RV; give the referred-to SVPERL_MAGIC_backref
magic if it hasn't already; and push a back-reference to this RV onto the array of backreferences associated with that magic. If the RV is magical, set magic will be called after the RV is cleared.SV* sv_rvweaken(SV *sv)
ぐむー。ここにすべて説明してあります。めげずに実際の実装をみてみましょう。
SV * Perl_sv_rvweaken(pTHX_ SV *sv) { SV *tsv; if (!SvOK(sv)) /* let undefs pass */ return sv; if (!SvROK(sv)) Perl_croak(aTHX_ "Can't weaken a nonreference"); else if (SvWEAKREF(sv)) { if (ckWARN(WARN_MISC)) Perl_warner(aTHX_ packWARN(WARN_MISC), "Reference is already weak"); return sv; } tsv = SvRV(sv); sv_add_backref(tsv, sv); SvWEAKREF_on(sv); SvREFCNT_dec(tsv); return sv; }
さまざまなチェックを無視すると,主要な挙動は,
- 参照先に自分を backref として登録 via
sv_add_backref()
- 自分に
SVprv_WEAKREF
フラグをセット - 参照先の
REFCNT
を decrement
ですね。
sv_add_backref($tsv, $sv)
((sv_add_backref()
と反対に PERL_MAGIC_backref
から削除する sv_del_backref()
もあります。)) の実装は省略しますが(sv.c
にあります),
- まだ
PERL_MAGIC_backref
が存在しなければ,新しい AV(配列)をPERL_MAGIC_backref
として$tsv
に登録 $sv
をPERL_MAGIC_backref
として登録されている AV(配列)に push
という動作です。
んーとややこしくなったので整理します。
use Scalar::Util qw( weaken ); $a = \$b; $b = \$a; # $a ===> $b, $b ===> $a weaken($a); # $a ---> $b, $b ===> $a
とすると,
$a
のREFCNT
は 2 のままです (by scope, and, by$b
)WEAKREF
フラグが立ちます
$b
のREFCNT
は 1 になります (by scope, only)MAGIC
化され,PERL_MAGIC_backref[]
に$a
を登録します
なぜ weak reference の保持元に WEAKREF
フラグを立てるのか
weak reference の参照先の REFCNT
がすでに 1 減ってるから,保持元が DESTROY
されるときに気をつけないといけないからです。
たとえば,
use strict; use warnings; use Scalar::Util qw( weaken ); my $b = "Hello"; { my $a = \$b; # $a->{REFCNT} = 1; # $b->{REFCNT} = 2; weaken( $a ); # $a ---> $b # $a->{REFCNT} = 1; # $b->{REFCNT} = 1; } # DESTROY $a; # will decrement ($b as $a->{RV})->{REFCNT} ? # ??? print $b, "\n";
こんなコードがあった場合,WEAKREF
フラグを考慮せずに安直に $a->{RV}
の REFCNT
を decrement しちゃうと $b
が意図せず DESTROY
されちゃいますよね。
じっさいには,WEAKREF
フラグを考慮して以下のようなインプリメントになってます。
void Perl_sv_clear(pTHX_ register SV *sv) { /******* snip snip snip *******/ switch (SvTYPE(sv)) { /******* snip snip snip *******/ case SVt_PV: case SVt_RV: if (SvROK(sv)) { if (SvWEAKREF(sv)) sv_del_backref(sv); else SvREFCNT_dec(SvRV(sv)); } else if /* ...... snip snip snip ...... */ break; /******* snip snip snip *******/ }
weak reference の場合,参照先を SvREFCNT_dec()
するのではなく,backref から自身を取り除いています。
実証コード?も用意してみました*2。WEAKEN
フラグをセットしない bad_weaken()
です。
use strict; use warnings; my $b = "Hello"; { my $a = \$b; bad_weaken( $a ); } # ??? print $b, "\n"; use Inline C => <<'END_C'; void bad_weaken(SV *sv) { Inline_Stack_Vars; SvREFCNT_dec(SvRV(sv)); Inline_Stack_Void; } END_C
実行すると,
Use of uninitialized value in print at test.pl line 12. Attempt to free unreferenced scalar: SV 0x81546a8, Perl interpreter: 0x8153008.
のように,$b
が undef になってしまいます。
なぜ weak reference の参照先に保持元を backref として登録するのか
weak reference の参照先が DESTROY
したときに,保持元(リファレンス)の参照先を無効にしてあげるため,です。
たとえば,
use strict; use warnings; use Scalar::Util qw( weaken ); my $a; { my $b = "Hello"; $a = \$b; weaken( $a ); # $a ---> $b } # what should be going on? print $$a, "\n";
このようなコードを実行すると(正しい weaken()
のインプリメントでは)
Can't use an undefined value as a SCALAR reference at test.pl line 14.
のように表示されます。$a
が undef
であるかのように言われていますね。
$b
が DESTROY
する際に,backref を見て「保持元」を操作してます。でないと,$a
は $b
(の残骸)を参照したままになってしまいますからね。
なぜ保持元の登録に MAGIC
を利用するのか
なぜ MAGIC
化しているのか。PERL_MAGIC_backref
に back reference の配列を格納したいというのもありますが,free
時 の callback として Perl_magic_killbackrefs()
という関数を登録したいからです。この callback を使えるのが MAGIC
の良いところです。
なお Perl_magic_killbackrefs()
という関数ではなにをやっているかというと,PERL_MAGIC_backref[]
に登録された個々の sv について
RV
を NULL 化し(つまりリファレンスの参照先を NULL にする)undef
化し(と書くとDESTROY
するようですが,単にフラグ上undef
と等価にするということです)WEAKEN
フラグを落として
います。
あなたは私をみてたのね。私はいなくなるからもう私を見ないで。
といったところでしょうか。
weaken()
で気をつけるところ
weaken()
を使用するときに気をつけなければいけないのは,たとえば
use Scalar::Util qw( weaken ); my $a = { next => ..., prev => ..., }; weaken( $a );
のように誤って・誤解して・安直に書いてしまうと,$a
が undef
になりうるところです。
$a = SV(RV):{ REFCNT => 1, RV => HV:{ REFCNT => 1, ...... } }
このような構造なので,$a
を weaken()
すると RV
先の HV
の REFCNT
が 1 減ってしまい,HV
が DESTROY
されちゃいます。
必ずしも weaken()
に頼る必要はない
双方向連結リストや環状リストのように単純な循環参照であれば,weaken()
に頼る必要はありません。各オブジェクトを統括するマネージャクラスに「構造」を「外在化」すれば循環参照は発生しません*3。Perlクックブック〈VOLUME1〉 の 13.13 節では環状リストのマネージャクラスを Ring class としてインプリメントしている例がでてきます。
ただし id:naoya 氏のような例では(後から)構造を外在化するのは難しいので素直に weaken()
を使用したほうがよいでしょう。
おわりに
んー個人的には CPAN module 書いたり,プロジェクトで多用されるモジュールを書き下ろしたりするのでなければ,global destruction があるので循環参照についてそんなに気にする必要もないかなぁと思います。でも mod_perl みたいに永続的環境とか,処理中に大量のインスタンスを生成・破棄する場合とか困るか。無責任なことをいってすみません。
念のため,メモリリークや循環参照の検出ツールを挙げると(全部使ったことないですごめんなさい),
- [ Perl ] Devel::Cycle | nDiki (Naney さんによる調査)
- Devel::Cycle
- Devel::Leak (定番)
- 上に含まれないやつで気になるもの
- Devel::TrackObjects
show_tracked()
で好きなときにだせる
- Devel::Leak::Object
- 自力で
\*STDOUT
をリダイレクトしてstatus()
を呼べば途中でもいける?
- 自力で
- Devel::GC::Helper
- Devel::LeakTrace::Fast
- んー
INIT
フェイズで収集しといてEND
フェイズで検査・表示,だから mod_perl には向かないかな
- んー
- Devel::TrackObjects
mod_perl だと PerlCleanupHandler
とかにリーク検出を設ければいいのかな。みなさんはどうしてますか*4。