0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Swift] 大量データ処理の罠!`.lazy` を使ってメモリコピーと無駄な計算を回避するリファクタリング

0
Posted at

iOSアプリのパフォーマンス改善やリファクタリングにおいて、AIから .lazy(遅延評価列)の使用を提案されることがあります。

これは大量のデータを扱う際、通常の mapfilter が引き起こす「配列全体のメモリコピー(新しい配列の生成)」と「無駄なループ計算」を回避するための非常に有効なアプローチらしいです。


パフォーマンスに問題のあるコード(アンチパターン)

10万件のユーザーデータから「アクティブユーザー」を絞り込み、さらに「画面表示用のモデル(ViewModel)」に変換して、最終的に最初の3件だけを取得したいケースを考えます。

import Foundation

// 巨大な元データ(10万件)
struct User {
    let id: Int
    let isActive: Bool
    let name: String
}
let largeUsersArray: [User] = (1...100000).map { User(id: \$0, isActive: \$0 % 2 == 0, name: "User\(\$0)") }

struct UserViewModel {
    let displayName: String
}

// メモリとCPUを浪費する処理
func getTopThreeActiveUsers() -> [UserViewModel] {
    let result = largeUsersArray
        .filter { \$0.isActive }                       // ここで約5万件の新しい配列がメモリに生成される
        .map { UserViewModel(displayName: \$0.name) }   // さらに5万件の別配列が生成される
    
    return Array(result.prefix(3))                    // 結局使うのは最初の3件だけ
}

何が問題なのか?

  1. 中間配列によるメモリコピーの連鎖
    .filter を実行した時点で、条件に合う5万件のデータを格納する新しい配列がメモリ上に丸ごと生成(コピー)されます。続く .map でも、さらに新しい5万件の配列が作られます。
  2. 無駄な計算コスト
    最終的に欲しいのは prefix(3)(最初の3件)だけです。それなのに、後ろに続く残り 49,997 件に対しても、フィルター評価とビューモデルへの変換処理をすべて実行してしまっています。

.lazy を使った改善コード(リファクタリング後)

配列の後に .lazy を1行挟むだけで、メモリコピーと無駄な計算をすべてスキップできます。

func getTopThreeActiveUsersOptimized() -> [UserViewModel] {
    let lazyResult = largeUsersArray.lazy             // ここで lazy(遅延評価)にする
        .filter { \$0.isActive }                       // まだ計算されない
        .map { UserViewModel(displayName: \$0.name) }   // まだ計算されない
        .prefix(3)                                    // ここで「3件取れれば終了」という条件が決まる
    
    // 最後に Array にキャストした瞬間に、初めて先頭から順番に計算が走る
    // 3件分のアクティブユーザーが見つかった時点で処理は完全にストップする
    return Array(lazyResult)
}

なぜこれで改善するのか?

  1. メモリコピーの回避(生成される配列は1つだけ)
    .lazy をつけると、.filter.map を呼んでも新しい配列は一切作られません。「こういうルールで後から順に計算してね」という指示書(LazyMapSequence などの特殊な型)が作られるだけなので、メモリ消費はほぼゼロです。
  2. 必要最低限の計算(オンデマンド実行)
    最後の Array(lazyResult) で通常の配列に戻す際、先頭の要素から順番にチェックが始まります。「アクティブなユーザーが3人見つかった」その瞬間にすべてのループ処理が終了します。残りの9万9千件以上のデータは、1ミリも計算されません。

まとめ:通常の配列処理と .lazy の違い

項目 通常の配列処理 .lazy を使った処理
メモリ消費 処理(map等)の数だけ新しい配列が作られ、メモリを喰う 中間配列を作らないため、メモリをほぼ消費しない
計算量 全要素(10万件なら10万件すべて)を強制実行 prefix などを使えば、必要な分だけで計算終了
戻り値の型 Array<T> LazyFilterSequence などの特殊な型(※Array()で戻せる)

注意点:銀の弾丸ではない

  • .lazy は結果をキャッシュ(保存)しません。作成した lazyResult の要素に何度も繰り返しアクセスする場合、その都度再計算が走って逆に遅くなります
  • 今回の改善例のように、「一連の処理の最後に Array() で一気に通常配列に書き出す」 または 「1回だけ for-in で回して使い捨てる」 という使い方が最も効果的です。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?