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 に説明があります。

sv_rvweaken

Weaken a reference: set the SvWEAKREF flag on this RV; give the referred-to SV PERL_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)
http://perldoc.perl.org/perlapi.html#SV-Body-Allocation

ぐむー。ここにすべて説明してあります。めげずに実際の実装をみてみましょう。

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;
}

さまざまなチェックを無視すると,主要な挙動は,

  1. 参照先に自分を backref として登録 via sv_add_backref()
  2. 自分に SVprv_WEAKREF フラグをセット
  3. 参照先の REFCNT を decrement

ですね。

sv_add_backref($tsv, $sv)((sv_add_backref() と反対に PERL_MAGIC_backref から削除する sv_del_backref() もあります。)) の実装は省略しますが(sv.c にあります),

  1. まだ PERL_MAGIC_backref が存在しなければ,新しい AV(配列)を PERL_MAGIC_backref として $tsv に登録
  2. $svPERL_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 から自身を取り除いています。


実証コード?も用意してみました*2WEAKEN フラグをセットしない 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.

のように表示されます。$aundef であるかのように言われていますね。

$bDESTROY する際に,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 );

のように誤って・誤解して・安直に書いてしまうと,$aundef になりうるところです。

$a = SV(RV):{
    REFCNT => 1,
    RV => HV:{
        REFCNT => 1,
        ......
    }
}

このような構造なので,$aweaken() すると RV 先の HVREFCNT が 1 減ってしまい,HVDESTROY されちゃいます。

必ずしも weaken() に頼る必要はない

双方向連結リストや環状リストのように単純な循環参照であれば,weaken() に頼る必要はありません。各オブジェクトを統括するマネージャクラスに「構造」を「外在化」すれば循環参照は発生しません*3Perlクックブック〈VOLUME1〉 の 13.13 節では環状リストのマネージャクラスを Ring class としてインプリメントしている例がでてきます。

ただし id:naoya 氏のような例では(後から)構造を外在化するのは難しいので素直に weaken() を使用したほうがよいでしょう。

おわりに

んー個人的には CPAN module 書いたり,プロジェクトで多用されるモジュールを書き下ろしたりするのでなければ,global destruction があるので循環参照についてそんなに気にする必要もないかなぁと思います。でも mod_perl みたいに永続的環境とか,処理中に大量のインスタンスを生成・破棄する場合とか困るか。無責任なことをいってすみません。

念のため,メモリリークや循環参照の検出ツールを挙げると(全部使ったことないですごめんなさい),

mod_perl だと PerlCleanupHandler とかにリーク検出を設ければいいのかな。みなさんはどうしてますか*4

*1:といっても nicovideo 視聴ですが。

*2:Inline が必要です。

*3:ただし,各オブジェクトからマネージャインスタンスに逆参照を貼ってしまうと循環参照が発生します。

*4:gdb でトレースするとか!?→gdbでXS on mod_perlをデバッグ - stanaka