0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】LINQでやりがちな落とし穴とパフォーマンス改善Tips【実務向け】

0
Posted at

はじめに

LINQは可読性が高く生産性を上げてくれる強力なツールですが、遅延実行・多重列挙・不適切なデータ構造などでパフォーマンス問題を招きやすいです。
この記事では、実務でよく遭遇する落とし穴を再現コードとともに示し、すぐ使える改善策を具体例で解説します。コードはそのままコピペして試せます。


よくある落とし穴 1 多重列挙による無駄な再評価

問題の例

var query = items.Where(x => x.IsActive);

int count = query.Count();      // クエリを評価
var first = query.FirstOrDefault(); // 再度クエリを評価

query は遅延実行されるため、Count()FirstOrDefault()同じ処理が2回走る

対処法

  • 一度だけ評価して結果を保持する(ToList()ToArray() を使う)。
var list = items.Where(x => x.IsActive).ToList();
int count = list.Count;
var first = list.FirstOrDefault();

注意点ToList() はメモリを消費するため、データ量に応じて使い分ける。


よくある落とし穴 2 不要な ToList の乱用

問題の例

// 毎回 ToList() を呼んでいる
var names = users.Select(u => u.Name).ToList();
var filtered = names.Where(n => n.StartsWith("A")).ToList();

ToList() を多用するとメモリ使用量が増え、GC負荷が高まる。

対処法

  • 必要なタイミングでのみ即時評価する。
  • パイプライン内で絞り込みや投影を先に行う。
var filtered = users
    .Where(u => u.IsActive)
    .Select(u => u.Name)
    .Where(n => n.StartsWith("A"))
    .ToList(); // 最後に一度だけ評価

よくある落とし穴 3 Where と Select の順序ミス

問題の例

// 先に Select してから Where を適用すると無駄が生じる場合がある
var result = items.Select(x => new { x.Id, x.Value })
                  .Where(x => x.Value > 100)
                  .ToList();

投影(Select)で重い処理を先に行うと、絞り込み(Where)で減らせるはずのコストを払ってしまう。

対処法

  • 可能なら先に Where で絞る。
var result = items.Where(x => x.Value > 100)
                  .Select(x => new { x.Id, x.Value })
                  .ToList();

よくある落とし穴 4 LINQ to Objects と LINQ to Entities の違いを無視する

問題の例

// EF Core の DbSet に対してローカルメソッドを使うとクエリがクライアント評価される
var q = context.Users
    .Where(u => IsValid(u.Name)) // IsValid はローカルメソッド
    .ToList();

ローカルメソッドはデータベース側で評価できないため、全件取得してからクライアント側で評価される。

対処法

  • DB側で評価できる式にするか、必要な列だけ取得してからローカル処理する。
// DB側で評価可能な式にする
var q = context.Users
    .Where(u => u.Name.StartsWith("A"))
    .ToList();

// あるいは必要列だけ取得してからローカル評価
var names = context.Users.Select(u => u.Name).ToList();
var filtered = names.Where(n => IsValid(n)).ToList();

よくある落とし穴 5 Join や GroupBy で巨大な中間結果を作る

問題の例

var joined = from o in orders
             join i in items on o.Id equals i.OrderId
             select new { o, i };

var list = joined.ToList(); // 中間結果が巨大になる可能性

対処法

  • 必要な列だけを投影する。
  • 大きな結合は DB 側で行い、ページングやフィルタを活用する。
var list = context.Orders
    .Where(o => o.CreatedAt >= DateTime.Today.AddDays(-7))
    .Select(o => new
    {
        o.Id,
        o.Total,
        Items = o.Items.Select(i => new { i.Id, i.Price })
    })
    .ToList();

よくある落とし穴 6 PLINQ の安易な導入

問題の例

var results = items.AsParallel()
                   .Select(x => HeavyCompute(x))
                   .ToList();

PLINQ は並列化のオーバーヘッドがあり、小さな処理や I/O バウンド処理では逆に遅くなる。

対処法

  • CPU バウンドでかつ十分に重い処理に限定して使う。
  • 並列度を制御する。
var results = items.AsParallel()
                   .WithDegreeOfParallelism(Environment.ProcessorCount)
                   .Select(x => HeavyCompute(x))
                   .ToList();

パフォーマンス改善テクニック集

1. 計測を最初に行う

  • StopwatchBenchmarkDotNet でボトルネックを特定する。
var sw = Stopwatch.StartNew();
var result = query.ToList();
sw.Stop();
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms");

2. 必要な列だけ取得する

  • 大きなオブジェクトを丸ごと取得せず、DTO に投影する。
var dto = context.Users
    .Where(u => u.IsActive)
    .Select(u => new UserDto { Id = u.Id, Name = u.Name })
    .ToList();

3. ルックアップは HashSet / Dictionary を使う

  • Contains を大量に呼ぶ場合は HashSet に変換して O(1) にする。
var ids = new HashSet<int>(largeList.Select(x => x.Id));
var filtered = items.Where(x => ids.Contains(x.Id)).ToList();

4. 多重列挙を避ける

  • クエリを複数回評価する場合は ToList() で一度確定する。

5. 遅延評価を意識する

  • IEnumerable<T>IQueryable<T> の評価タイミングを理解する。

6. 大量データはチャンク処理する

  • メモリに乗り切らない場合は分割して処理する。
const int batchSize = 1000;
int skip = 0;
List<Item> batch;
do
{
    batch = context.Items.OrderBy(i => i.Id).Skip(skip).Take(batchSize).ToList();
    Process(batch);
    skip += batchSize;
} while (batch.Count > 0);

実務チェックリスト

  • 計測してから最適化している
  • 多重列挙をしていない
  • ToList の使用箇所を意図的に決めている
  • DB クエリとローカル処理の境界を明確にしている
  • 必要列だけを取得している
  • HashSet/Dictionary を適切に使っている
  • PLINQ は慎重に導入している
  • 大きな結合はページングや分割で対処している

まとめ

  • LINQ の便利さの裏には「評価タイミング」と「データ量」に関する落とし穴がある。
  • まずは計測し、多重列挙を避ける必要列だけ取得する適切なデータ構造を使うことが最も効果的。
  • 小さな改善を積み重ねることで、可読性を保ちながらパフォーマンスを大きく改善できる。

おわりに

この記事があなたのコードのボトルネック発見と改善に役立てば嬉しいです。
よければストック・フォローお願いします!

0
0
1

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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?