iOSアプリのパフォーマンス改善やリファクタリングにおいて、AIから .lazy(遅延評価列)の使用を提案されることがあります。
これは大量のデータを扱う際、通常の map や filter が引き起こす「配列全体のメモリコピー(新しい配列の生成)」と「無駄なループ計算」を回避するための非常に有効なアプローチらしいです。
パフォーマンスに問題のあるコード(アンチパターン)
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件だけ
}
何が問題なのか?
-
中間配列によるメモリコピーの連鎖
.filterを実行した時点で、条件に合う5万件のデータを格納する新しい配列がメモリ上に丸ごと生成(コピー)されます。続く.mapでも、さらに新しい5万件の配列が作られます。 -
無駄な計算コスト
最終的に欲しいのは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つだけ)
.lazyをつけると、.filterや.mapを呼んでも新しい配列は一切作られません。「こういうルールで後から順に計算してね」という指示書(LazyMapSequenceなどの特殊な型)が作られるだけなので、メモリ消費はほぼゼロです。 -
必要最低限の計算(オンデマンド実行)
最後のArray(lazyResult)で通常の配列に戻す際、先頭の要素から順番にチェックが始まります。「アクティブなユーザーが3人見つかった」その瞬間にすべてのループ処理が終了します。残りの9万9千件以上のデータは、1ミリも計算されません。
まとめ:通常の配列処理と .lazy の違い
| 項目 | 通常の配列処理 |
.lazy を使った処理 |
|---|---|---|
| メモリ消費 | 処理(map等)の数だけ新しい配列が作られ、メモリを喰う | 中間配列を作らないため、メモリをほぼ消費しない |
| 計算量 | 全要素(10万件なら10万件すべて)を強制実行 |
prefix などを使えば、必要な分だけで計算終了 |
| 戻り値の型 | Array<T> |
LazyFilterSequence などの特殊な型(※Array()で戻せる) |
注意点:銀の弾丸ではない
-
.lazyは結果をキャッシュ(保存)しません。作成したlazyResultの要素に何度も繰り返しアクセスする場合、その都度再計算が走って逆に遅くなります。 - 今回の改善例のように、「一連の処理の最後に
Array()で一気に通常配列に書き出す」 または 「1回だけfor-inで回して使い捨てる」 という使い方が最も効果的です。