overload の rebless バグについて

きちんと追いきれなかったので結構ぐだぐだです。

そもそも bless の挙動とは

#!/usr/bin/perl
use strict;
use warnings;

package FooBar;

package main;

my $a = { };
my $b = $a;

print $a, "\n";
# HASH(0xb8002a0)

print $b, "\n";
# HASH(0xb8002a0)


bless $a, 'FooBar';

print $a, "\n";
# FooBar=HASH(0xb8002a0)

print $b, "\n";
# FooBar=HASH(0xb8002a0)

前半はまぁいいでしょう。同じ無名ハッシュリファレンスをさしているので同じ内容が出力されます。

おもしろいのは後半です。

$abless しただけなのに,$bbless されています。

実は bless というのはリファレンス変数自身を bless するのではなく,参照先の実体を bless しているんですね。実際ソースを追うと,デリファレンス先の実体を OBJECT 化し,クラスと関連付けています。

overload を rebless したときのバグとは

では,クラス FooBar の文字列化演算子overload してみましょう。

バグのある素 Perl 5.8.8 でためしてみます。

#!/usr/bin/perl
use strict;
use warnings;

package FooBar;
use overload q{""} => sub { 'Hi!' };

package main;

my $a = { };
my $b = $a;

print $a, "\n";
# HASH(0x166a32a0)

print $b, "\n";
# HASH(0x166a32a0)


bless $a, 'FooBar';

print $a, "\n";
# Hi!

print $b, "\n";
# FooBar=HASH(0x166a32a0)

先ほどと同じように $bFooBarbless されていますが,$a の出力は overload されているのに $boverload されていません。

なぜそのようなことがおこったのか

Perl 5.8.8 時点では,overload されたというフラグは,リファレンス自身(RV)の MAGIC の一種として実装されています。リファレンス自身の器としては $a$b は別物ですから,$abless した際に overload MAGIC は立っても $b の MAGIC は立たなかったのです。

対策案

じゃあどうすればいいのか。

ということで Yappo さんも慧眼として指摘してますが

でも現状の対策としてはどうすればいいのかちょっと思いつかないですね。リファレンスを捜索してoverloadフラグを立てる、なんてことをするモジュールとか作れるのかしら・・・。

overloadと再blessの問題 - Unknown::Programming

のような方策が Bug #34925 for perl5: overload and rebless の議論で考えられ,patch#27512 として結実しました。


でも要するに何かが bless される度に,全スカラーを走査して,該当するスカラーに MAGIC を立てるという処理ですから重いですよね。

実際には

  • RV の MAGIC の状態が変わらない bless の場合には処理を行わない
  • いくつの RV が影響をうけるかについては参照カウンタによって算出ができるので,該当するものが見つかれば即終了させる

などの高速化が図られています。ですが overload されているクラスのインスタンスを新規に生成する場合は前者に当てはまらないですから,遅くなるわけです。

RedHat の対応

Fedora 5 あたりでなぜだか RedHatpatch#27512 をあてました。

ところが先ほど述べたように overload されたクラスのインスタンス生成が遅くなってしまいました。ということで皆さんもご存知のように非難囂々となったわけです(⇒Bug 196836 – perl-5.8.8-5 is 30X slower than perl-5.8.8-4)。

その後の対応については Bug #43283 for perl5: Reblessing overloaded objects incurs significant performance penalty より抜粋。

rnorwood> RT#34925 見て patch#27512 あてたら遅くなったよ
rnorwood> たすけて

nicholas> patch#27512 って Perl-devel 用のもんだよ。
nicholas> このバグ patch#28775 でなおってるよ。
nicholas> これ Fedora の Perl にあててある?

rnorwood> おお patch#28775 なんてのあるのね。気づかんかった。
rnorwood> うまくいったら報告するよ

rnorwood> patch#28775 でうまくいったお!

rnorwood というのは Robin Norwood @ RedHat,nicholas というのは Nicholas Clark です。

ということで,Fedora 7, 8 以降「については」遅くならないパッチ patch#28775 があてられ,問題とはならなくなりました。

patch#28775 って何?

このパッチ,浅く追ってみましたが,特に patch#27512 で遅くなることの対策,というわけではありません。

Perl のオペコード(Perl VM の擬似命令みたいなもの)を拡張して,無名ハッシュリファレンスや無名配列リファレンスの生成を速くする(3 ops が 1 op に)ものです。

たぶん,リファレンスだとあらかじめ分かっている場合に,一度空配列を作ってそれのリファレンスを作って,とわざわざやるんじゃなくて一気に生成している感じです。

これでなぜ遅くならなくなるのかは残念ですが不明です。

未整理な憶測

patch#27512 より抜粋。

static void
S_reset_amagic(pTHX_ SV *rv, const bool on) {
    /* It is assumed that you've already turned magic on/off on rv  */
    SV* sva;
    SV *const target = SvRV(rv);
    /* Less 1 for the reference we've already dealt with.  */
    U32 how_many = SvREFCNT(target) - 1;
    MAGIC *mg;

    if (SvMAGICAL(target) && (mg = mg_find(target, PERL_MAGIC_backref))) {
        /* Back referneces also need to be found, but aren't part of the
           target's reference count.  */
        how_many += 1 + av_len((AV*)mg->mg_obj);
    }

    /******** snip ********/
}

たぶん patch#28775 をあててない環境だと上記引用文中の if 文が無駄に TRUE になることがあるんだけど,パッチをあてると(なぜか)治る,んだと思うんだ。あるいは SvREFCNT(target) - 1 が無駄にでかいとか?

上記パッチで参照カウンタの増加が抑制されるのかな?。

ちなみに Perl 5.9.4 での対策

overload されているかどうか,というのはそもそも本当はクラスの属性です。これをリファレンス自身の MAGIC で表現していたことが誤りなので,bless と同様に,参照先の MAGIC で表現するように変更しました。結構大胆な変更ですね(具体的には SvAMAGIC_on, SvAMAGIC_off というマクロのレベルで対処しています)。

つまり Perl 5.9.4 では patch#27512 が当たっているわけではなく,根本から書き直されています。

まとめ

  • Perl 5.8.8 は overload rebless のバグがある
  • Fedora 6 と RHEL 5 の Perl 5.8.8 にはこのバグのパッチがあてられているが,そのために overload を利用したクラスのインスタンス生成等で遅くなるというエンバグがある
  • Fedora 7, 8 ではさらにパッチをあてて遅くならないようにしている
  • RHEL 5.2 の updates 15 でパッチがあててあり遅くならない*1
  • Perl 5.9.4 以降ではそれらのパッチではなく,根本的な改善がなされている