ソフトウェア設計

Swiftのフライウェイトパターン:オブジェクト共有でメモリを節約する方法

アプリを作っていると、似たようなオブジェクトを数千、数万個も生成することがあります。地図上に数万個のマーカーを配置する機能が代表例で、このような場合はメモリグラフが上がり続けやすくなります。

読了 5 分
Swiftのフライウェイトパターン:オブジェクト共有でメモリを節約する方法のカバー画像

アプリを作っていると、似たようなオブジェクトを数千、数万個も生成することがあります。地図上に数万個のマーカーを配置する機能が代表例で、このような場合はメモリグラフが上がり続けやすくなります。

そんなときに検討したいのがフライウェイトパターンです。複数のオブジェクトが共通して使うデータを1つにまとめて共有し、メモリ使用量を大幅に減らす手法です。同じものを毎回作り直さず、作成済みの1つを一緒に使います。

この記事では、Swiftにおけるフライウェイトパターンの概要、適した場面、実際のコードでの実装方法まで解説します。

フライウェイトパターンとは?

Flyweightは「軽い物体」という意味です。ボクシングで最も軽い階級の名前でもあります。

考え方は単純です。オブジェクトが持つデータを2種類に分けます。

  • 内在的状態(intrinsic):複数のオブジェクトで共有できる、変化しないデータ。木の種類、テクスチャ、色などが例です。
  • 外在的状態(extrinsic):オブジェクトごとに異なり、その都度変化するデータ。木の位置や大きさなどが例です。

森を描くと考えてみましょう。100万本の木を描くとしても、実際には松、オーク、カエデの3種類だけだったらどうでしょうか?

テクスチャと色の情報(内在的状態)は3つだけ作って共有し、位置の値(外在的状態)だけを100万個別々に持てばよいのです。

重い共通データは1つだけ作って共有し、軽い個別データだけを別々に管理する。これがフライウェイトのすべてです。

あの日、マーカーを配置して跳ね上がるメモリと、しばらく格闘していました。
あの日、マーカーを配置して跳ね上がるメモリと、しばらく格闘していました。

Swiftのコードで実装してみましょう

言葉だけではイメージしにくいので、すぐにコードを見てみましょう。先ほどの木の例をそのままコードにします。

まず、共有する内在的状態を保持するフライウェイトオブジェクトです。

// 共有される重いデータ(内在的状態)
final class TreeType {
    let name: String
    let color: String
    let texture: Data  // 容量の大きいテクスチャ

    init(name: String, color: String, texture: Data) {
        self.name = name
        self.color = color
        self.texture = texture
    }
}

次に、これらのTreeTypeを管理して共有するファクトリを作ります。すでに作成済みの種類があれば、新たに作らずそのまま使います。

// 同じ種類は新しく作らず再利用
final class TreeFactory {
    private var pool: [String: TreeType] = [:]

    func treeType(name: String, color: String, texture: Data) -> TreeType {
        let key = "\(name)-\(color)"
        if let existing = pool[key] { return existing }
        let type = TreeType(name: name, color: color, texture: texture)
        pool[key] = type
        return type
    }
}

実際の木は位置などの外在的状態だけを持ち、重いデータはTreeTypeを参照します。

ファクトリが保持し、木は参照だけを持ち歩く構造です。
ファクトリが保持し、木は参照だけを持ち歩く構造です。
// 個々の木は位置だけを持ち、種類は参照で共有
struct Tree {
    let x: Double
    let y: Double
    let type: TreeType  // 共有オブジェクトへの参照
}

木を100万本植えても、TreeTypeのインスタンスは種類の数(例:3つ)だけ存在します。残りはすべて、その3つを指す参照にすぎません。

森全体を描いても、テクスチャは3つだけ共有すれば十分でした。
森全体を描いても、テクスチャは3つだけ共有すれば十分でした。

いつ使い、いつ使わないべきか?

フライウェイトが常に正解とは限りません。使わないほうがよい場合も多くあります。状況ごとに整理します。

状況 フライウェイト
同じ性質のオブジェクトを非常に大量に生成する 適している
オブジェクトが共有可能な重いデータを持つ 適している
オブジェクト数が少ない 不要(むしろ複雑さが増すだけ)
状態の大半がオブジェクトごとに異なる 効果は小さい

大量のオブジェクトと共有可能な重いデータ。この2つの条件が重なるときこそ、フライウェイトの出番です。

Swiftでは、classは参照型としてすでに参照で共有されます。そのため、値型のstructに重いデータを丸ごと入れて何度もコピーしてしまう状況で特に効果を発揮します。


よくある質問

Q. Singleton Patternとは何が違いますか?

Singletonはクラスのインスタンスを1つだけに強制するパターンです。フライウェイトは複数のインスタンスを持ちながら、共有できるものだけを選んで共有します。木の種類は3つ、木そのものは100万本、という具合です。

Q. キャッシュと混同してしまいます。

目的が異なります。キャッシュは「再計算・再取得したくない」ために結果を保存し、フライウェイトは「メモリを節約する」ために共有オブジェクトを管理します。ただし、ファクトリのpoolはキャッシュのように動作します。

Q. Swift Standard Libraryでも使われていますか?

はい。文字列インターニングや小さな整数のキャッシュなど、似た発想の最適化があります。同じ値を毎回新しく作らず、共有するのです。


フライウェイトパターンは名前こそ難しそうですが、発想はとても単純です。「重複する重いものは1つだけ作って共有しよう」ということです。

地図のマーカー、パーティクル、タイルマップ、テキストレンダリングのように、似たオブジェクトを大量に扱う必要があるなら、一度検討してみてください。メモリグラフが目に見えて落ち着くのを実感できるはずです。

あわせて読みたい