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)
前半はまぁいいでしょう。同じ無名ハッシュリファレンスをさしているので同じ内容が出力されます。
おもしろいのは後半です。
$a
を bless
しただけなのに,$b
も bless
されています。
実は 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)
先ほどと同じように $b
も FooBar
に bless
されていますが,$a
の出力は overload
されているのに $b
は overload
されていません。
なぜそのようなことがおこったのか
Perl 5.8.8 時点では,overload
されたというフラグは,リファレンス自身(RV
)の MAGIC の一種として実装されています。リファレンス自身の器としては $a
と $b
は別物ですから,$a
を bless
した際に 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 あたりでなぜだか RedHat は patch#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 が当たっているわけではなく,根本から書き直されています。