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
SvWEAKREFflag on this RV; give the referred-to SVPERL_MAGIC_backrefmagic 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。