渋谷ほととぎす通信

エンジニア社長によるUnityとAIのブログ & エンジニアの生存戦略

【Unity】LINQとfor文の負荷検証どっちを使うべき?

【Unity】LINQとfor文の負荷検証どっちを使うべき?

こんにちは、Unityエンジニアのオオバです。

お悩みさん
お悩みさん
  • LINQって便利だけど重い?
  • forとLINQってどっちが軽い?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    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とfor文の負荷検証どっちを使うべき?_0

    Unityエディタで計測するとノイズが入り不安定な結果になったためです。

    では早速検証を進めていきます。各LINQの検証コードとその結果を記述しています。

    最後に分かりやすくまとめていますので、結果だけ見たい方は記事後半までジャンプしてください。

    FirstOrDefaultの場合

    Profiler.BeginSample("### FirstOrDefault ###");  
    for (int i = 0; i < count; i++)  
    {
        list.FirstOrDefault(x => x == searchKey);  
    }
    Profiler.EndSample();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_1

    Firstの場合

    Profiler.BeginSample("### First ###");  
    for (int i = 0; i < count; i++)  
    {
        list.First(x => x == searchKey);  
    }
    Profiler.EndSample();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_2

    FirstOrDefaultとFirstはほぼほぼ同じ結果となりました。

    Anyの場合

    Profiler.BeginSample("### Any ###");  
    for (int i = 0; i < count; i++)  
    {
        list.Any(x => x == searchKey);  
    }
    Profiler.EndSample();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_3

    意外だったのが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();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_4

    Where後ToListの場合

    Profiler.BeginSample("### Where and ToList ###");  
    for (int i = 0; i < count; i++)  
    {
        list.Where(x => x == searchKey).ToList();  
    }
    Profiler.EndSample();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_5

    ToArrayに比べてToListの方がメモリ確保量が多いことが分かりました。

    Whereだけの場合

    Profiler.BeginSample("### Where only ###");  
    for (int i = 0; i < count; i++)  
    {
        list.Where(x => x == searchKey);  
    }
    Profiler.EndSample();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_6

    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();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_7

    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();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_8

    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();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_9

    IList型foreach文の場合

    Profiler.BeginSample("### foreach(IList)###");  
    for (int i = 0; i < count; i++)  
    {
        foreach(var a in ilist)  
        {
            if(a == searchKey)  
            {
                break;  
            }
        }
    }
    Profiler.EndSample();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_10

    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();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_11

    Array型foreach文の場合

    Profiler.BeginSample("### foreach(Array)###");  
    for (int i = 0; i < count; i++)  
    {
        foreach(var a in array)  
        {
            if(a == searchKey)  
            {
                break;  
            }
        }
    }
    Profiler.EndSample();  
    

    【Unity】LINQとfor文の負荷検証どっちを使うべき?_12

    以上のような検証結果になりました。これらの結果を表にまとめてみます。

    検証結果まとめ表

    処理名処理時間GC Alloc
    First3.39ms16.4KB
    FirstOrDefault3.27ms16.4KB
    Any3.41ms16.4KB
    Where後ToArray9.63ms27.7KB
    Where後ToList9.48ms28.1KB
    Whereのみ0.08ms19.5KB
    List型のfor文0.78ms0KB
    List型のforeach0.80ms0KB
    IList型のfor文1.56ms0KB
    IList型のforeach3.00ms3.9KB
    配列のfor文0ms0KB
    配列foreach0ms0KB

    上記の結果から以下の点がポイントだと分かりました。

    • 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の結果があなたのゲーム開発に役立ったら嬉しいです。

    オススメ記事
    検証環境
    • Unity6000.0.32f1
    • iPhone16Pro iOS18.3.1