[Swift][Architecture] ValueType な Model の使い所を考えてみた

     
ValueType な Model の使い所をあらためて考えてみました

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.1
  • Xcode 13.2.1
  • iOS 15.2

以下では、DB という用語を CoreData や Realm をイメージして使用しています。一般(?)の DB とは少し使い方が違うかもしれません。

ValueType な Model

ValueType な Model が万能ではないことに気付いたので、使い所を考えてみます。以下では、特別な実装をしていない ValueType な Model を想定して考えていきます。

ValueType な Model を使用するメリット

ValueType を使うことの利点がそのまま適用できます。

Apple のドキュメントは、こちら

以下の記事でも説明しましたが、ValueType な Model を使うと、UNDO/REDO の実装が非常に簡単になります。
[Swift]Value-type なモデルを使った UNDO の実装(その2: Value-type で モデルを作成)

ValueType な Model を使用するデメリット

ですが、もちろん 不便な点も存在するわけです。

特に、Model として使用する時には、以下のようなデメリットが考えられます。

・全ての要素をメモリ上に持つ必要がある
・変更は全て、Model に依頼して変更する必要がある

MEMO

2つ目は、メリットを別の視点で見ているだけですが、CoreData や Realm が変更をすべて MOC や Realm に投げないといけないことと似ていることに気づきます。

1つめデメリットをもう少し考えてみるために、DB が何をしてくれているのかを考えてみます。

DB のしていること

DB は、非常に多くの機能を提供してくれていますが、重要な機能の1つとして、全ての要素をメモリ上にロードせずに扱うことができる が挙げられます。

具体的には、DB は、処理に必要な要素のみをロードして、不必要な要素をロードしない という扱いができます。
不必要と判断されてメモリ上にロードされていない状態は、Fault 状態と呼ばれます。

その後、必要と判断された時には、DB が自動的にロードしてくれます。

Fault 管理

つまり、DB がしてくれることの1つとして、Fault 状態管理 があります。

Apple のドキュメントは、こちら

Fault管理の必要性

非常に多くの要素があるときに、そのデータ全てをメモリ上にロードしてくることは物理的にもできません。OSが仮想メモリをサポートしているとしても、現実的とは思えません。

全ての要素をメモリ上にロードできないとすると、必要な要素を必要に応じてロードするということになります。
逆に、不要になった要素はメモリ上から外すことも必要になります。ポイントは、不要になる ≠ 削除 ということです。
メモリにシビアなモバイル環境では、画面上に表示されなくなった時点で不要ということもあるかもしれません。

この Fault 管理ですが、思ったよりも複雑になります。なぜなら、通常 要素間はリンクされていることもあるので、要素の必要/不必要は 要素単体での判断だけでなく、リンク先の要素の必要/不必要の判断にも影響されるからです。例えば 要素自体は不要と判断されても、必要と判断された要素からリンクされているため 実は必要かもしれません。このような構造は、(グラフ理論の)グラフと言われるもので、CoreData が Object Graph Management という表現をされる謂れでもあります。

ValueType な Model での Fault

ValueType な Model で Fault 状態管理することを考えてみます。

ValueType でも Optional は保持できますので、Optional を使ってみます。


struct Element: Identifiable {
  var id: UUID
  var elementData: Data?
}

常に、ID でアクセスされ、elementData が .none である時は、データを読み込んでくる というような形になるでしょう。

上記のような形を使うことで、ValueType な Model でも Fault状態管理はできそうに感じます。

ですが、ポイントは、”データを読み込んでくる”にあります。

1つの要素の読み込みが必要となった時に、ディスクにあるデータ全体を読み込んで、必要な要素を見つけ、その要素のみをメモリに残し 他要素を破棄する というのは現実的ではありません。

やはり、ディスクにあるデータの特定箇所のみの読み込みが可能 であることが必要になるでしょう。

つまり、”データを読み込んでくる” を可能とするには、”部分的なデータを読み込むことが可能な” Persistent Layer が別に必要になります。

つまり、Fault 状態管理を必要とする ValueType な Model は Persistent Layer が必要となります。

Persistent layer

CoreData や Realm を使用する理由は様々なものが考えられると思いますが、先の Graph Management をして Fault 管理してくれることはその大きな理由の1つですが別の理由がないか考えてみます。

シンプルな Persistent Layer から考えてみます。

JSON ファイル v.s. CoreData/Realm

シンプルな Persistent Layer という意味では 素のファイルシステムが代表かと思います。フィあるとして JSON ファイルを考えてみます。(Text ファイルでも良いです)

ValueType な Model を JSON として保存することは非常に容易です。Codable に対応させて encode すれば OK です。

様々な型のデータは(何らかの変換が必要となるケースもありますが)、JSON フォーマットで保存することができます。

Persistent Layer としての DB のメリット

DB では、以下のことが可能です。
・指定データのみの読み込み
・指定データのみの更新
・指定データのみの削除

上記の点が JSON ファイルでどのようになるかを考えてみます。

JSON 視点で書き直すと、以下です。
・特定要素を読み込むにもファイル全体の読み込みが必要
・データの一部アップデートで、ファイル全体の更新が必要
・データの一部削除は、ファイル全体の更新が必要
# 以降では上記の機能を ファイル部分処理 と呼びます。

Fault 状態管理をしたい ValueType な Model を JSONファイル(ファイルシステム) と組み合わせるのには課題がありそうです。

Persistent Layer と ValueType な Model の相性

ValueType な Model を適切な Persistent Layer と組み合わせることを考えてみます。

の前に、そんな都合の良い Persistent Layer はあるのでしょうか?

期待する Persistent Layer は、”Fault 管理はしてくれるが、それ以上はしない” というものです。

探してみるとわかりますが、大抵 DB 機能がメインでそのために、Fault 管理をしているケースが大半です。

つまり、”Fault 管理してくれる Persistent Layer を選ぶともれなく DB 機能も付いてくる” ということです。

DB の機能も持つ Persistent Layer を ValueType と組み合わせることのメリットは少ないです。

ということで、ValueType な Model を DB 的な Persistent Layer と組み合わせるのはアイデアとしてはアリかもしれませんが、現実的にはナシに思えます。

ということは、JSON ファイルのようなシンプルな Persistent Layer との組み合わせが ValueType な Model の使い所に思えてきます。

ValueType な Model の使い所

ValueType/Fault 状態管理/ Persistent Layer の特徴を見てきたところで、ValueType な Model の使い所は、以下になりました。

  • 要素数が あまり大きくないデータしか扱わないケース

すべてオンメモリで持つことに問題はないわけですから、部分ロード等は不要です。

UNDO/REDO がシンプルになる等のメリットも享受できますので、このケースが、ValueType な Model の使い所と思えます。

だけだとアレなので、ではどれくらいの要素数が境界になるのかを考えてみました。

計測してみた

全てをオンメモリで持つとどのくらいメモリを使用するのか測ってみました。

計測環境と対象モデル

計測環境

macMini2018(intel) + 16G memory + macOS Monterey 12.1 + Xcode 13.2.1
測定方法:Xcode 上の “Debug Navigator” の Memory Report を確認
(Memory Use で表示されている値ではなく、High で表示されている値)

MemoryMeasurement

対象モデル1

UUID と Int を持つ以下のような要素を対象としました。


struct Element: Identifiable {
  var id: UUID = UUID()
  var intValue: Int = Int.random(in: 0..<1_000_000)
}

対象モデル2

UUID,Int,String を持つ以下の要素も測定しました


struct Element: Identifiable {
  var id: UUID = UUID()
  var intValue: Int = Int.random(in: 0..<1_000_000)
  var doubleValue: Double = Double.random(in: 0..<1_000_000.0)
}

計測に使ったコード


//
//  ViewModel.swift
//
//  Created by : Tomoaki Yagishita on 2021/12/15
//  © 2021  SmallDeskSoftware
//

import Foundation
import SwiftUI

@main
struct StructDBPerformanceApp: App {
    @StateObject var viewModel = ViewModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
struct Element: Identifiable {
    var id: UUID = UUID()
    var intValue: Int = Int.random(in: 0..<1_000_000)
    var stringValue: String = ["Hello", "Hallo", "こんにちわ"].randomElement()!
}

class ViewModel: ObservableObject {
    let elementDB: [Element]
    public init() {
        // 10_000_000 will vary
        self.elementDB = Array(repeating: Element.init(), count: 10_000_000) 
    }
}

計測結果

それぞれのモデルで、100万、500万、1,000万、5,000万を測定しています。

1,000,000 5,000,000 10,000,000 50,000,000
Int 47.8MB 139.3MB 253.6MB 1.14GB
Int+String 62.6MB 214.7MB 405.3MB 1.89GB

まとめ:ValueType な Model の使い所

ValueType な Model は非常に扱いやすいのですが、以下のようなケースに限定的に有効であると理解した方が良さそうです。

ValueType な Model の使い所
  • ValueType な Model は、要素数が少ないケースで有効に活用しよう
    • シンプルなモデルであれば、1000万要素くらいまで
    • 複雑なモデルであれば、100万要素くらいまで

許容できる要素数や許容できるメモリ量は個人的な感覚です。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です