C# の LINQ パフォーマンスの注意点
LINQは便利だが、使い方を誤るとパフォーマンスが大きく低下することがある。ここでは注意すべきポイントと対策を解説する。
複数回の列挙を避ける
遅延評価のクエリを複数回列挙すると、その都度クエリが実行される。
var numbers = Enumerable.Range(1, 1000);
var query = numbers.Where(n =>
{
Console.WriteLine($"Checking {n}"); // 1000回 × 2 = 2000回呼ばれる
return n % 2 == 0;
});
int count = query.Count(); // 1回目の列挙
var list = query.ToList(); // 2回目の列挙解決策は、一度 ToList() や ToArray() で結果を確定させること。
var query = numbers.Where(n => n % 2 == 0).ToList(); // 1回だけ列挙
int count = query.Count; // List.Count プロパティ
var list = query; // すでにリストCount() vs Any()
要素の存在確認には Count() > 0 より Any() を使う。
全要素を数える。O(n)。
最初の要素で即座に true を返す。O(1)。
var largeList = Enumerable.Range(1, 1000000);
// 悪い例:100万回ループ
bool hasElements1 = largeList.Count() > 0;
// 良い例:1回で終わる
bool hasElements2 = largeList.Any();Where と FirstOrDefault の順序
検索条件を Where + First に分けると非効率になることがある。
var people = GetLargePeopleList();
// やや非効率
var alice1 = people.Where(p => p.Name == "Alice").FirstOrDefault();
// より効率的
var alice2 = people.FirstOrDefault(p => p.Name == "Alice");実際にはどちらも遅延評価で同様に動作するが、後者の方が意図が明確で、将来的な最適化の恩恵を受けやすい。
ToList の適切なタイミング
ToList() を早すぎる段階で呼ぶと、中間結果がすべてメモリに載る。
// 悪い例:100万件のリストを作ってからフィルタ
var result1 = data.ToList().Where(x => x.IsActive).Take(10);
// 良い例:フィルタしてから必要な分だけ取得
var result2 = data.Where(x => x.IsActive).Take(10).ToList();不要な OrderBy
結果の並び順が重要でない場合、OrderBy は省略する。
// 悪い例:並べ替え不要なのにソート
bool hasAdmin = users.OrderBy(u => u.Name).Any(u => u.Role == "Admin");
// 良い例:ソートなし
bool hasAdmin = users.Any(u => u.Role == "Admin");ソートは O(n log n) のコストがかかる。
Contains vs HashSet
大量の値との照合には HashSet を使う。
var targetIds = new List<int> { 1, 2, 3, /* ... 1000個 */ };
var users = GetUsers(); // 10000件
// 悪い例:O(n × m) = 1000万回の比較
var result1 = users.Where(u => targetIds.Contains(u.Id));
// 良い例:O(n) = 1万回の比較
var targetIdSet = new HashSet<int>(targetIds);
var result2 = users.Where(u => targetIdSet.Contains(u.Id));List.Contains は O(n)、HashSet.Contains は O(1) だ。
GroupBy の後の検索
GroupBy の結果を何度も検索するなら、ToDictionary に変換する。
var orders = GetOrders();
// 悪い例:顧客IDで検索するたびにグループを走査
var grouped = orders.GroupBy(o => o.CustomerId);
var customer1Orders = grouped.FirstOrDefault(g => g.Key == 1);
// 良い例:Dictionaryで O(1) 検索
var ordersByCustomer = orders.GroupBy(o => o.CustomerId)
.ToDictionary(g => g.Key, g => g.ToList());
var customer1Orders = ordersByCustomer.GetValueOrDefault(1);Select での重い処理
Select 内で重い処理をすると、列挙のたびに実行される。
var items = GetItems();
// 悪い例:SelectでAPI呼び出し
var enriched = items.Select(item =>
{
var detail = FetchDetailFromApi(item.Id); // 毎回API呼び出し
return new { item, detail };
});
// enriched を2回列挙するとAPIも2回呼ばれるAPI呼び出しのような副作用のある処理は、事前に実行して結果をキャッシュすべきだ。
メモリ効率
大量データを扱う場合、すべてをメモリに載せないようにする。
// 悪い例:100万件を一度にメモリに載せる
var allData = File.ReadAllLines("huge.csv")
.Select(ParseLine)
.ToList();
// 良い例:ストリーミングで処理
var result = File.ReadLines("huge.csv")
.Select(ParseLine)
.Where(x => x.IsValid)
.Take(100);ReadLines は遅延評価で1行ずつ読み込む。ReadAllLines は全行を一度に読み込む。
パフォーマンス計測
最適化の前に、実際にボトルネックを特定することが重要だ。
var sw = Stopwatch.StartNew();
var result = expensiveQuery.ToList();
sw.Stop();
Console.WriteLine($"実行時間: {sw.ElapsedMilliseconds}ms");推測で最適化するより、計測して問題箇所を特定してから対処する方が効果的だ。過度な最適化は可読性を損なうので、必要な場合にのみ行うべきである。