こんにちは、Unityエンジニアのオオバです。
C#を使う上でLINQは非常に便利な機能なのは言うまでもありません。
しかし実際のプロダクトへ組み込む際の負荷は知っておくべきです。便利でソースコードの見通しは良いけど激重だったみたいなことがあってはいけませんよね。
そこで本記事ではLINQとfor文の負荷を比較していきます。自分のゲームにLINQを使うかどうか迷っている方はぜひ参考にしてみてください。
👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!
LINQとは
LINQは「Language Integrated Query」の略で、C# コレクションやデータベースのデータを直感的に扱えるクエリ言語で、データを「検索」「並べ替え」「抽出」するときにとても便利です。
int[] numbers = { 1, 2, 3, 4, 5 };
// LINQを使って偶数を抽出
var evens = numbers.Where(n => n % 2 == 0).ToArray();
foreach (var num in evens)
{
Debug.Log(num); // 2, 4
}
例えば上記のコードは1〜5の配列ですが、偶数だけを抽出した配列を1行で書けます。このようにLINQはC#でデータを簡単に検索・操作するための仕組みということです。
便利なLINQなのですがそれなりに負荷もかかるため、本記事ではパフォーマンスを計測してどう付き合っていくかを考察していきます。
検証する対象LINQ
今回の検証対象のLINQは使用頻度が高い次のフィルタ系にしぼりました。
- FirstOrDefault/First
- Any
- Where
検証方法はUnityProfilerです。項目で言うと「Time ms(実行時間)」 と 「GC Alloc」です。実行時間は文字通りで、高ければ高いほどFPSの低下につながります。
GC Allocは1フレーム辺りのヒープメモリを確保する容量です。数値が高いとGCの発生回数が増えてしまい、プロダクトのパフォーマンスを大きく下げてしまう場合があります。なぜならGCが発生するとゲーム中の大きなカクつきにつながることが多いからです。
またfor、foreachを計測していきますが、コレクションの種類もList、IList、Arrayの3種類で検証していきます。
また今回の検証では数値を可視化しやすくするため以下の条件にしています。
- 配列要素数を10000個
- 1フレーム内の実行回数を100回
UnityProfilerで検証するため、検証箇所に「Profiler.BeginSample」と「Profiler.EndSample」を仕込んでいきます。
検証デバイスはiPhone16Proとしました。
Unityエディタで計測するとノイズが入り不安定な結果になったためです。
では早速検証を進めていきます。各LINQの検証コードとその結果を記述しています。
最後に分かりやすくまとめていますので、結果だけ見たい方は記事後半までジャンプしてください。
FirstOrDefaultの場合
Profiler.BeginSample("### FirstOrDefault ###");
for (int i = 0; i < count; i++)
{
list.FirstOrDefault(x => x == searchKey);
}
Profiler.EndSample();
Firstの場合
Profiler.BeginSample("### First ###");
for (int i = 0; i < count; i++)
{
list.First(x => x == searchKey);
}
Profiler.EndSample();
FirstOrDefaultとFirstはほぼほぼ同じ結果となりました。
Anyの場合
Profiler.BeginSample("### Any ###");
for (int i = 0; i < count; i++)
{
list.Any(x => x == searchKey);
}
Profiler.EndSample();
意外だったのがAnyです。FirstOrDefault, Firstとほぼ同じ結果だから。コレクション内に存在するかしないかboolを返すだけなので最も処理負荷が軽くなると思っていたためです。
Where後ToArrayの場合
Profiler.BeginSample("### Where and ToArray ###");
for (int i = 0; i < count; i++)
{
list.Where(x => x == searchKey).ToArray();
}
Profiler.EndSample();
Where後ToListの場合
Profiler.BeginSample("### Where and ToList ###");
for (int i = 0; i < count; i++)
{
list.Where(x => x == searchKey).ToList();
}
Profiler.EndSample();
ToArrayに比べてToListの方がメモリ確保量が多いことが分かりました。
Whereだけの場合
Profiler.BeginSample("### Where only ###");
for (int i = 0; i < count; i++)
{
list.Where(x => x == searchKey);
}
Profiler.EndSample();
Whereだけの場合は高速ですが、それでもメモリは確保するということを覚えておきましょう。
List型for文の場合
Profiler.BeginSample("### for(List) ###");
for (int i = 0; i < count; i++)
{
for(var j = 0; j < list.Count; j++)
{
if(list[j] == searchKey)
{
break;
}
}
}
Profiler.EndSample();
List型foreach文の場合
Profiler.BeginSample("### foreach(List)###");
for (int i = 0; i < count; i++)
{
for(var j = 0; j < list.Count; j++)
{
if(list[j] == searchKey)
{
break;
}
}
}
Profiler.EndSample();
IList型for文の場合
Profiler.BeginSample("### for(IList)###");
for (int i = 0; i < count; i++)
{
for(var j = 0; j < ilist.Count; j++)
{
if(list[j] == searchKey)
{
break;
}
}
}
Profiler.EndSample();
IList型foreach文の場合
Profiler.BeginSample("### foreach(IList)###");
for (int i = 0; i < count; i++)
{
foreach(var a in ilist)
{
if(a == searchKey)
{
break;
}
}
}
Profiler.EndSample();
Array型for文の場合
Profiler.BeginSample("### for(Array)###");
for (int i = 0; i < count; i++)
{
for(var j = 0; j < array.Length; j++)
{
if(array[j] == searchKey)
{
break;
}
}
}
Profiler.EndSample();
Array型foreach文の場合
Profiler.BeginSample("### foreach(Array)###");
for (int i = 0; i < count; i++)
{
foreach(var a in array)
{
if(a == searchKey)
{
break;
}
}
}
Profiler.EndSample();
以上のような検証結果になりました。これらの結果を表にまとめてみます。
検証結果まとめ表
処理名 | 処理時間 | GC Alloc |
---|---|---|
First | 3.39ms | 16.4KB |
FirstOrDefault | 3.27ms | 16.4KB |
Any | 3.41ms | 16.4KB |
Where後ToArray | 9.63ms | 27.7KB |
Where後ToList | 9.48ms | 28.1KB |
Whereのみ | 0.08ms | 19.5KB |
List型のfor文 | 0.78ms | 0KB |
List型のforeach | 0.80ms | 0KB |
IList型のfor文 | 1.56ms | 0KB |
IList型のforeach | 3.00ms | 3.9KB |
配列のfor文 | 0ms | 0KB |
配列foreach | 0ms | 0KB |
上記の結果から以下の点がポイントだと分かりました。
- LINQは使用時にメモリを確保
- for, foreachはゼロアロケート(メモリを確保が0)
- 中でもArrayが最速
- First, FirstOrDefault, Anyはほぼ同じパフォーマンス
- ToArrayよりToListの方がメモリ使用量が高い
- IListはforeachのみメモリを確保するため注意
こんな感じです。
それでもオオバはLINQを使う
LINQと比べるとfor、foreachの方が圧倒的に高速で、メモリも確保せずに健全な状態になることが分かりました。
しかし、オオバはLINQを使い続けます。理由はソースコードの見通しが良くなるからです。LINQで書くと2行で終わるところがfor文では数行に渡ることなんて普通です。見通しの良さはゲーム開発で非常に重要。
大事なのは使い所だと考えています。パフォーマンスを求める箇所にLINQは不適合なので使いません。例えばバトル中、絶対にカクつきを起こさせたくないときはLINQは使わないでしょう。
しかし、ゲーム起動時や場面切り替えタイミングの暗転時などは気にせずLINQを使ってよいと考えています。
見通しの良いコードは開発効率に大きな影響を与えるので、使い所を見極めて積極的に取り入れていきましょう。
結局、パフォーマンスは計測してみないとわかりません。計測してもう少し改善したいなっていうときにLINQの使用を見直すでも良いと思います。
まとめ
本記事ではLINQとforの負荷検証をしてきました。
予想通りパフォーマンスだけで言うとfor/foreachの勝利に終わりました。メモリも確保しないためGC対策もできています。
しかし可読性から見るとLINQの方が圧倒的に有利です。記述コード量が大幅に削減できます。
どちらが良い悪いではありません。状況によって答えは変わります。最近のスマホ高速なので、そこまで繊細にLINQに対して敏感になる必要もないかもしれません。
これは実際に作ったゲームをプロファイルしてみないとわからないのです。
LINQをfor文に書き直すことは、ただのパフォーマンスチューニングです。パフォーマンスチューニングが必要かどうかは計測してみないとわかりません。
だから闇雲にLINQは重いからfor文に書き換えようと思う必要はありません。パフォーマンスチューニングする必要があるからLINQを見直すというスタンスが良いのではないかと思います。
今回紹介したLINQの結果があなたのゲーム開発に役立ったら嬉しいです。

筆者のXをフォローしよう
- Unity6000.0.32f1
- iPhone16Pro iOS18.3.1