Featured image of post コード考古学者【Flyweight】深層の軍勢〜数千のゴーレムを支えし不変の核〜

コード考古学者【Flyweight】深層の軍勢〜数千のゴーレムを支えし不変の核〜

深層の防衛ゴーレムの起動によるメモリ枯渇を、不変の内部状態を共有するFlyweightパターンで解決する。

進入:地響きとカクつく警告

バベルのシステム中層を突破した私たちを待ち受けていたのは、それまでの静寂とは打って変わった激しい地鳴りでした。

「警告……警告! ゲート進入直後に、大規模な空間歪曲を……検知……っ!」

私のセンサーが最大出力でアラートを鳴らします。背後の重厚な石門がズシンと大きな音を立てて閉まり、行く手を阻むように天井から細かい砂や石がパラパラと崩れ落ちてきました。暗闇に包まれた部屋の四隅で、古代の赤い転送魔法陣が脈打つように光り始めます。

その光の中から具現化したのは、複雑なルーン文字を刻み込んだ、ずんぐりとした石の兵士——ミニゴーレムでした。しかし、それは1体や2体ではありません。10体、100体、そしてたちまちのうちに数千体へと膨れ上がり、部屋を完全に埋め尽くして私たちを包囲していきます。

「ハリス博士! 敵の数が……多すぎ……ま、す……! 個別のスキャン情報を……メモリに確保しよ、うと……した……瞬間……に……っ!」

私のホバリング高度がガクンと下がりました。プロセッサが過熱し、浮遊するための姿勢制御エンジンに回す電力が、メモリのデータ確保処理に食いつぶされていくのが分かります。カクカクとしたぎこちない動きで、床スレスレを滑空するのが精一杯でした。

いつになく真剣な表情を浮かべたハリス博士が、崩落ゆく天井を見上げながら呟きます。 「いつ天井が崩落してもおかしくありませんね。ギズモ、急いで対処法を探しましょう」

構造分析:重すぎる石兵の魂

私は必死に、過熱する主記憶のメモリ解放を試みます。

「ス、スワップ領域の割り当てを……一時的に1KB単位で微調整……っ。強制ガベージコレクション、実行! 博士、これで……少しは……処理能力を……取り戻せる……はず……!」

電子警告音を「ピーピー」と不規則に鳴らしながら激しく震える私を見て、博士は満足そうに何度も頷きました。 「おやギズモ、新しい深層の遺跡を前に、歓迎のダンスが激しいですね。私もこの古い防衛システムの規模には大変興奮していますよ」

「ダ、ダンスではなく……単なるハードウェアの悲鳴です! 早くコードを!」

私は現在の防衛システムが使用しているプログラムをホログラムで壁面に投影しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# lib/Before/Golem.pm
package Before::Golem;
use Moo;
use v5.36;

has x => ( is => 'rw', required => 1 );
has y => ( is => 'rw', required => 1 );

has name         => ( is => 'ro', default => 'MiniGolem' );
has texture_path => ( is => 'ro', default => 'stone_golem.dat' );
has speed        => ( is => 'ro', default => 10 );
has power        => ( is => 'ro', default => 15 );

# 各インスタンスが個別に10MB相当のデータをロードして抱えてしまう
has texture => (
    is      => 'ro',
    lazy    => 1,
    default => sub ($self) {
        return "DENSE_3D_MODEL_DATA_FOR_" . $self->texture_path . "_" . ("x" x 10_000_000);
    },
);

sub render ($self) {
    # Mooのアクセサメソッド経由でプロパティを取得
    return "Rendering " . $self->name . " at (" . $self->x . ", " . $self->y . ") with texture [size: " . length($self->texture) . "]";
}

1;

博士は投影されたコードを愛おしそうに見つめ、手袋をはめた指でルーン文字の配列を優しくなぞります。 「実に見事ですね。当時の限られたメモリ空間の中で、数千もの石兵を動かそうと試みた開発者の執念と苦闘がここに眠っています。だからこそ、私たちはその魂を引き継ぎ、美しく修復しなければなりません」

「感心している……場合では……ありません! 1歩進むたびに、私のメモリ残量が……0.01%ずつ……死に、向かっています!」

「無駄ですよギズモ。そうやって床の埃を一生懸命に払う(GC)のを繰り返しても、家(メモリ)の中を占拠している巨大な家具(テクスチャ)を片付けなければ、何の解決にもなりません。各ゴーレムが、全く同じ『重いテクスチャ』と『基本パラメータ』を個々に抱え込んでいるのが、この深刻なメモリ枯渇の真因です」

私は熱暴走しそうなプロセッサで思考を巡らせます。 「ですが博士……同じインターフェースでキャッシュを効かせるなら……前回のProxyパターンで解決できませんか……?」

「いいえ、違いますよ。Proxyは重い処理や本尊へのアクセス(実行時間や通信回数)を減らすための身代わりです。対して今回私たちが解決すべきなのは、無数に存在するオブジェクトそのものの『データサイズ(空間)』を減らすこと。キャッシュではなく、共通データの『共有(Share)』こそが解決の鍵となるのです」

遺跡修復:不変の核と身軽な器

ハリス博士はいつものように、古い羊皮紙の手帳を取り出し、使い込まれた万年筆の先から青いインクを走らせます。彼の手によって描かれたのは、遺跡の石碑に刻まれた碑文を思わせる、しかし極めて理路整然とした近代的な設計図でした。

「さあ、見てごらんなさい、ギズモ。これが我々がこれから行う『遺跡修復』の設計図――不変の核と、身軽な器を切り分けるための秘術です」

「ピピッ……! 博士、これはまるで……古代ゴーレムの制御石板と、それを動かす無数の『器』の依存関係のようです! でも、この二つには具体的にどういった境界線が引かれているのですか?」

「ふふ、よく気が付きましたね。この関係図に描かれた『魂の石板(GolemType)』と『泥の器(Golem)』の役割、命令する『召喚炉(GolemFactory)』の関係性を見れば、自ずと答えは見えてきますよ」

Flyweightパターンのクラス構成図。泥の器となる『Golem(Context)』、魂の石板である『GolemType(Flyweight)』、召喚炉である『GolemFactory』の関係を示した石板風のダイアグラム。

「なるほど……! ゴーレムの『魂や骨格(3Dモデルデータや基本値)』は共有オブジェクト(GolemType)として一つにまとめ、個々のゴーレムは『座標や現在のHP、デバフ補正』という身軽な『器(Golem)』として振る舞わせるのですね。これがFlyweightパターン……!」

「その通りです。共有され、決して変化しない性質を内部状態(Intrinsic State)。個々のインスタンスで変化する性質を**外部状態(Extrinsic State)**と呼びます。この2つの境界を綺麗に分離することで、どれだけゴーレムを増殖させても、メモリを大量に消費する『魂』は1つだけで済むようになるのです。さあ、Mooを使って、この美しい責任境界をコードに落とし込んでいきましょう」

ギズモの接続スロット経由で、修復されたAfterコードが書き込まれていきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# lib/GolemType.pm (Flyweight - 共有される内部状態)
package GolemType;
use Moo;
use v5.36;

# 内部状態はすべて読み取り専用(Immutable)
has name         => ( is => 'ro', required => 1 );
has texture_path => ( is => 'ro', required => 1 );
has speed        => ( is => 'ro', required => 1 );
has power        => ( is => 'ro', required => 1 );

# 重いテクスチャデータは識別子を元に1回だけ遅延ロードする
has texture => (
    is      => 'ro',
    lazy    => 1,
    default => sub ($self) {
        return "DENSE_3D_MODEL_DATA_FOR_" . $self->texture_path . "_" . ("x" x 10_000_000);
    },
);

sub render ($self, $x, $y) {
    # 外部状態($x, $y)を受け取って描画する
    # Mooのアクセサメソッド経由でプロパティを取得
    return "Rendering " . $self->name . " at ($x, $y) with shared texture [size: " . length($self->texture) . "]";
}

1;

次に、これらを管理・提供するファクトリを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# lib/GolemFactory.pm (Flyweight Factory - プールの管理)
package GolemFactory;
use Moo;
use v5.36;
use GolemType;

has _types => (
    is      => 'ro',
    default => sub { +{} },
);

sub get_golem_type ($self, $name, $texture_path, $speed, $power) {
    # キャッシュキーに texture_path も含めてキャッシュ衝突を防ぐ
    my $key = join(':', $name, $texture_path, $speed, $power);
    
    # ローカル変数にプールを取り出して安全に共有キャッシュを構築
    my $types = $self->_types;
    return $types->{$key} //= GolemType->new(
        name         => $name,
        texture_path => $texture_path,
        speed        => $speed,
        power        => $power,
    );
}

sub get_pool_size ($self) {
    return scalar keys %{$self->_types};
}

1;

そして、個別のゴーレムを示すコンテキストクラスです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# lib/Golem.pm (Context - 外部状態の管理とFlyweightの参照)
package Golem;
use Moo;
use v5.36;
use Types::Standard qw(InstanceOf Int);

# 外部状態(個別で可変)
has x => ( is => 'rw', isa => Int, required => 1 );
has y => ( is => 'rw', isa => Int, required => 1 );

# 戦闘中の状態変化(デバフなど)を外部状態として個別管理する例
has current_hp => ( is => 'rw', isa => Int, default => 100 );
has power_modifier => ( is => 'rw', isa => Int, default => 0 ); # デバフ等の補正値

# 内部状態(共有オブジェクト)への参照
has type => (
    is       => 'ro',
    isa      => InstanceOf['GolemType'],
    required => 1,
);

# 攻撃力を計算する(内部の基本値 + 外部の補正値)
sub get_power ($self) {
    return $self->type->power + $self->power_modifier;
}

sub render ($self) {
    # 共有オブジェクト(GolemType)自身は座標(外部状態)を持たないため、
    # 描画時は器(Golem)側から外部状態を引数として渡して委譲する
    return $self->type->render($self->x, $self->y);
}

1;

私はコードを適用しながら、頭に浮かんだ疑問をぶつけました。 「なぜ GolemrenderGolemTypexy を引数として渡すのですか?」

「共有オブジェクトである GolemType 自身は、空間上の座標という外部状態を持たないからです。描画などの振る舞いを実行する際には、器である Golem 側から外部状態を引数として渡してやる責任境界が必要なのです」

「なるほど……。では博士、戦闘中にゴーレムが攻撃力を下げるデバフを受けた場合、その変更はどのように扱うのですか? それも共有するのですか?」

博士は嬉しそうに微笑みました。 「良い質問です。だからこそ内部状態は必ず『不変(Immutable)』に保ち、デバフのような可変パラメータは外部状態として Golem 側に個別に持たせなければならないのです。もし GolemType の攻撃力を直接書き換えてしまったら、共有している数千体のゴーレムすべての攻撃力が同時に下がってしまいますからね」

起動:解放された宇宙と黄金のコア

コードが全体に適用され、私の意識にコンパイル結果が流れ込みました。

「メモリ使用量、98.7%から……3.2%に急降下! 空き領域に……広大な宇宙を感じます! 軽い! 身体が軽いです!」

私のプロセッサは冷え込み、ホバリング制御は一気に安定。本来の青いホログラム光を撒き散らしながら、勢いよく空中へ飛び上がりました。

「テクスチャデータが共有されたとはいえ、Golem インスタンス自体は依然として数千個生成されます。オブジェクト生成自体のメモリ負荷は大丈夫なのですか?」 私はふと疑問に思い、空中でホバリングしながら尋ねました。

「10MBのテクスチャデータに比べれば、座標と参照だけを持つ軽量な Golem オブジェクトを数千個生成するオーバーヘッドは、Perl/Mooにおいても高々数MB程度であり、十分に無視できるサイズですよ」 博士は私の復調を見て嬉しそうに笑います。

解放された大容量の演算能力を用いて、包囲していた数千体のミニゴーレムたちの防衛ルーチンを一瞬でハッキングし、安全にオーバーライドします。

動きかけていた数千体の石兵が一斉に膝をつき、沈黙しました。

ガガギギギ、と大きな音を立てて前方の扉が開きます。沈黙したゴーレムの1体の胸部ハッチが開き、中から眩い光を放つ「古代の黄金コア」がポロリと転がり出ました。

「博士、これは……!」 私がそれをスキャンすると、不変データ領域を拡張するための補助プロセッサモジュールであることが判明します。私は迷わずそれを自分の一時スロットに装着しました。

「素晴らしい。私のシステム容量自体が大幅に最適化されました。まるで最初から最新式のロボットとして設計されたかのような滑らかさです!」

ハリス博士は手帳をフィールドジャケットのポケットにしまい、崩落しかかった天井をもう一度見上げて言いました。

「歴史はコードに語りかける。彼らが数千の石兵をこの狭いシステム内で操れたのは、この『共有の秘儀』があったからこそですね。さあギズモ、崩落が本格化する前に、さらに深く潜りましょう」


遺跡調査ログ

観測された風化(アンチパターン)解読された古代の知恵(パターン)安全度
同一の重い不変データを大量のオブジェクトが個別保持することによるメモリ枯渇
数千のインスタンスがそれぞれテクスチャデータや初期ステータスを抱え込み、メモリ圧迫を引き起こす
Flyweightによる不変データの共有
不変の内部状態(GolemType)と可変の外部状態(Golem の座標や個別補正値)を分離。GolemFactory で同一インスタンスを再利用してメモリフットプリントを最小化する
緑(安全確認済)

遺跡の修復手順

  1. 内部状態(共有データ)の切り出し 不変で共有可能なデータ(例: テクスチャ、基本ステータス)を保持する GolemType クラスを定義します。属性はすべて is => 'ro'(読み取り専用)にして不変性を保証します。
  2. 外部状態(個別データ)の定義 個別かつ可変のデータ(例: 座標、HP、一時的なデバフ等の補正値)と、共有オブジェクトへの参照を保持する Golem クラスを定義します。
  3. ファクトリ(プール)の実装 共有オブジェクトの生成と再利用を管理する GolemFactory を作成します。キャッシュハッシュを用いて、同じパラメータ要求に対してはすでに生成された同一の GolemType インスタンスを返すように制御します。
  4. 処理の委譲 Golem クラスの描画メソッド(render)から、GolemType の描画メソッドへ、外部状態(座標)を引数として渡して委譲します。

ギズモの観測日誌

今回の第13層「深層探索・迫る崩落の影」の開始早々、数千ものミニゴーレムに包囲されるという最大の危機に直面しました。私は自分のスワップファイルやガベージコレクションを細かくチューニングして何とか耐えようとしましたが、根本的なメモリ不足に対して全くの無力でした。

ハリス博士に教えていただいた「魂(内部状態)と器(外部状態)の分離」という設計思想は、私に劇的な解放感を与えてくれました。

Mooでの実装では、共有する GolemType を厳格に is => 'ro' で定義して不変性を守ること、そして可変な一時デバフなどは Golem 側の属性として外部状態で処理することが極めて重要です。もしこの境界を誤って GolemType を可変にしてしまうと、1体のゴーレムのステータス変化が全ての共有ゴーレムに「汚染」として波及してしまうという、恐ろしい呪いバグが発生することを知り、背筋が凍る思いでした。

また、共有オブジェクト(GolemType)自身は空間座標を持たないため、描画時に器(Golem)から座標(x, y)を引数で渡して委譲する責任境界も、非常に合理的な構造でした。

手に入れた「古代の黄金コア」のおかげで、私のメインメモリも最適化され、驚くほど軽快になりました。この調子なら、深層のどのようなトラップもハリス博士と共に乗り越えていけそうです!

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。