【Unity】LINQのパフォーマンス検証

はじめに

はじめまして、2年目Unityエンジニアの大谷です。
この記事はカヤックUnityアドベントカレンダー2018の20日目の記事になります。
今回はUnityでLINQのパフォーマンスの比較をしてみました。

経緯

今回アドベントカレンダーを書いてみないかと勧められたものの、
僕はアドベントカレンダー初心者だったのでネタ探しに困っていました。
そんな中、LINQのパフォーマンス検証してみたら?
と、どこからかお告げを頂いたのでやってみることにしました。(圧倒的感謝...)

検証

今回検証するのは、LINQでよく使いそうなWhere(要素を絞り込む)、Select(全要素に対して処理)、OrderBy(並べ替え)
の3つにしてみました。比較するのは、それらの処理をforeachに置き換えたものにします。
また、LINQで操作した要素をList型に変換するToList()を使用するとパフォーマンスに大きく影響がでるらしいので、
そちらも別物として比較対象に追加します。まとめると、検証する対象になるのは

1. LINQ(IEnumerable型)
2. foreach(List型)
3. LINQ(List型)

の3つです。
検証用のサンプルコード(LinqTest.cs)はこちらになります。
Unityを用いた検証なので、Unity上で動作するようにします。

LinqTest.cs

using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

public class LinqTest : MonoBehaviour {

    void Update()
    {
        CompareSpeed();
    }

    void CompareSpeed()
    {
        var range1 = Enumerable.Range(1, 10000);
        var range2 = Enumerable.Range(1, 10000);
        var range3 = Enumerable.Range(1, 10000);

        Profiler.BeginSample("foreach処理");

        var list1 = new List<int>();

        foreach (var x in range1)
        {
            if (x % 2 == 0)
            {
                list1.Add(x * -10);
            }
        }

        list1.Sort();

        Profiler.EndSample();


        Profiler.BeginSample("LINQ処理");

        var list2 = range2.Where(x => x % 2 == 0)
                          .Select(x => x * -10)
                          .OrderBy(x => x);
        
        foreach (var x in list2)
        {
            // クエリを実行させる
        }

        Profiler.EndSample();


        Profiler.BeginSample("LINQ処理(ToList)");

        var list3 = range3.Where(x => x % 2 == 0)
                          .Select(x => x * -10)
                          .OrderBy(x => x)
                          .ToList();

        Profiler.EndSample();
    }
}

コードの補足

計測範囲はProfilerのSampleメソッドで囲まれているところになります。(Profiler.BeginSample(" ")とProfiler.EndSample()の間)。
このようにすると、指定した名称でUnityエディタのProfilerに表示されるようになり、その間の処理速度やGC Allocなどが見られます。ちなみに command + 7 でウィンドウが開きます。

where()やselect()を書いた段階ではクエリの式のみ保持されており、要素に対する処理はまだ行われていません。
なので、クエリの実行をさせる必要があります。LINQのクエリ実行タイミングは

1. foreachによるループ処理をする時
2. Single Maxなど一つの値を得る時
3. ToListなどで列挙する時

のようなので、foreachを回してクエリを実行させている部分があります。
では実際にUnityエディタのProfilerで結果を見てみましょう。

結果

Profilerをみてみるとこんな感じになりました。

editor

  LINQ LINQ(ToList) foreach
速度 5.8〜6.6ms 5.7〜6.5ms 2.4〜3.2ms
GC 123.4KB 187.8KB 64.4KB

CPU: foreach > LINQ(ToList) ≒ LINQ
メモリ: foreach > LINQ > LINQ(ToList)


の順でパフォーマンスが良さそうです。
速度に関しては、要素数を10000にしてようやくLINQとforeachの差が3.3〜3.4ms程になりました。
メモリに関しては、LINQ(ToList)は foreachの約3倍のGCを消費していました。
この差は実機でも変わらないのでしょうか?

ということで、実機だとどれほど違いが出るのか、iOSとAndroid端末で追加検証を行なっていきます。

実機で検証してみる

検証端末は、手元にあるiPhone8とXperia Z5 Compactを使用します。
実機でのプロファイリング方法はUnityの公式ドキュメントを参考にしました。今回はWiFiプロファイリングを行います。

iOS

1. iOS デバイスを WiFi ネットワークに接続  
2. UnityのBuild Settings ダイアログで"Development Build"と“Autoconnect Profiler” にチェック  
3. ケーブル経由で Mac にデバイスを接続し,Unity エディタで “Build & Run” をクリック  
4. デバイスでアプリが起動した後,Unity エディタで Profiler ウィンドウを開く  
5. 自動で接続されない場合は、ProfilerウィンドウのActive Profilerから接続されている端末を選択

Android

1. Androidデバイスで モバイルデータ通信 を無効化  
2. Android デバイスを WiFi ネットワークに接続  
3. UnityのBuild Settings ダイアログで"Development Build"と“Autoconnect Profiler” にチェック  
4. ケーブル経由で Mac/PC にデバイスを接続し,Unity エディタで “Build & Run” をクリック  
5. デバイスでアプリが起動した後,Unity エディタで Profiler ウィンドウを開く  
6. 自動で接続されない場合は、ProfilerウィンドウのActive Profilerから接続されている端末を選択

それぞれの結果はこちらです。

iPhone8

iphone 8

  LINQ LINQ(ToList) foreach
速度 4.9〜5.9ms 4.0〜5.3ms 1.9〜2.6ms
GC 123.4KB 187.8KB 64.4KB

CPU: foreach > LINQ(ToList) > LINQ
メモリ: foreach > LINQ > LINQ(ToList)

Xperia Z5 compact

xperia

  LINQ LINQ(ToList) foreach
速度 5.2〜5.6ms 5.0〜5.6ms 2.8〜3.3ms
GC 123.0KB 187.2KB 64.2KB

CPU: foreach > LINQ(ToList) ≒ LINQ
メモリ: foreach > LINQ > LINQ(ToList)


エディターと比較すると、
iPhone8ではLINQとforeachの差が3.0〜3.3ms程であまり変化はありませんでした。
一方、Xperia Z5 compactではLINQとforeachの差が2.3〜2.4msになり1msほど差が縮みました。
GCに関してはAndroidだとそれぞれ消費が若干少なくなりました。
これはAndroidのみ64bitではなく32bit版で動かしていることが原因のようです。
実際にAndroidを64bitで動かすとiOSと同じGC Alloc になりました。
数値にばらつきはあるものの、実機で改めて検証した甲斐があったのではないでしょうか。

LINQ自体はやはりクエリを実行するとエディター・実機関わらず処理が重くなったり大量にGCを消費してしまうようです。
使い分けるとすれば、
Updateなどで毎フレームに近く処理されるなら速度優先でforeach、
LINQを利用する場合はトレードオフを考えながら注意してから使う、
とするのが良さそうです。

LINQはforeachでの処理と比べて、簡潔にかけたり、どんな処理をしているのかが分かりやすくなる
などのメリットも多いので上手く使い分けられるようになれば取り入れていきたいですね。

おわりに

というわけでLINQのパフォーマンス検証でした。
明日は額田さんの「日本語テキストの自動改行」についての記事になります。
最後まで見ていただきありがとうございました。