LINQのパフォーマンスは遅いのか [Unity]

興味本位でLINQのパフォーマンスについて調べてみた.

LINQ とは

こちらの記事に詳しく書かれている.

tech.drecom.co.jp

Javaで言えば,Stream API的な.

こちらな記事でも書かれているんですが,LINQはデメリットとして,パフォーマンス低下を招きやすいというのがあります.
実際に定量的に調査してみたくなりました.

Unity 2018.1 かつ IL2CPP

JacksonDunstan氏が行ったパフォーマンス検証.

jacksondunstan.com

最もよく使われるLINQである,WhereSelectで以下の3つのパターンで比較.

  • manual バージョン (whereとselectの機能を手動で)
  • normal バージョン (whereとselectの機能をデリゲートで)
  • LINQ バージョン (whereとselectの機能をLINQで)

この時の検証環境は以下の通りだった.

  • Unity 2018.1.0f2
  • IL2CPP

検証コード

少しコードがおかしい部分があったので,コメント加筆しつつ修正.

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

public class LinqPerformanceTest : MonoBehaviour
{
    private string report;

    void Start()
    {
        int[] array = new int[1024];
        // 10000回繰り返す
        const int numIterations = 10000;

        Stopwatch stopwatch = new Stopwatch();

        // デリゲート宣言
        Func<int, bool> checker = val => val == 1;
        List<int> found = null;

        // ManualバージョンでWhereの時間を検証
        stopwatch.Reset();
        stopwatch.Start();
        for (int it = 0; it < numIterations; ++it)
        {
            int len = array.Length;
            found = new List<int>(len);
            for (int i = 0; i < len; ++i)
            {
                int elem = array[i];
                if (elem == 1)
                {
                    found.Add(elem);
                }
            }
        }
        long whereManualTime = stopwatch.ElapsedMilliseconds;

        // NormalバージョンでWhereの時間を検証
        stopwatch.Reset();
        stopwatch.Start();
        for (int it = 0; it < numIterations; ++it)
        {
            int len = array.Length;
            found = new List<int>(len);
            for (int i = 0; i < len; ++i)
            {
                int elem = array[i];
                if (checker(elem))
                {
                    found.Add(elem);
                }
            }
        }
        long whereNormalTime = stopwatch.ElapsedMilliseconds;

        // LinqバージョンでWhereの時間を検証
        stopwatch.Reset();
        stopwatch.Start();
        for (int it = 0; it < numIterations; ++it)
        {
            array.Where(checker).ToList();
        }
        long whereLINQTime = stopwatch.ElapsedMilliseconds;

        // デリゲート宣言
        Func<int, int> transformer = val => val * 2;
        List<int> transformed = null;

        // manual バージョンでWhereの時間を検証
        stopwatch.Reset();
        stopwatch.Start();
        for (int it = 0; it < numIterations; ++it)
        {
            int len = array.Length;
            transformed = new List<int>(len);
            for (int i = 0; i < len; ++i)
            {
                transformed.Add(array[i] * 2);
            }
        }
        long selectManualTime = stopwatch.ElapsedMilliseconds;

        // normal バージョンでWhereの時間を検証
        stopwatch.Reset();
        stopwatch.Start();
        for (int it = 0; it < numIterations; ++it)
        {
            int len = array.Length;
            transformed = new List<int>(len);
            for (int i = 0; i < len; ++i)
            {
                transformed.Add(transformer(array[i]));
            }
        }
        long selectNormalTime = stopwatch.ElapsedMilliseconds;

        // LINQ バージョンでWhereの時間を検証
        stopwatch.Reset();
        stopwatch.Start();
        for (int it = 0; it < numIterations; ++it)
        {
            array.Select(transformer).ToList();
        }
        long selectLINQTime = stopwatch.ElapsedMilliseconds;

        report =
            "Test,Manual Time,Normal Time,LINQ Time\n" +
            $"Where,{whereManualTime},{whereNormalTime},{whereLINQTime}\n" +
            $"Select,{selectManualTime},{selectNormalTime},{selectLINQTime}";
    }

    // 結果の表示
    void OnGUI()
    {
        GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report);
    }
}

結果

Unity 2018.1 の時はこんな感じでした.

f:id:xrdnk:20200507235053p:plain

f:id:xrdnk:20200507234959p:plain

Unity 5 時代で Scripting Backend が Monoだったときは,LINQは劇的に遅かったとあります.
こちらの記事の棒グラフを見ると頭が痛くなるほどの遅さであることがわかります.
jacksondunstan.com

Scripting BackEnd を IL2CPPにすることで劇的に早くなりました.

Unity 2019.3 かつ IL2CPP

では Unity 2019.3 ではどうか.検証してみた.
検証コードは上記と同じで,環境は Unity 2019.3.11f1 で IL2CPP.

結果

f:id:xrdnk:20200507235732p:plain

Whereは LINQ が一番早くなっている.
Select は Normal Time より早くなった.

実はLINQはUnity2019.3から早くなった説…?
というかmanualのタイム遅くなってないか…?

一応10回同様に測定してみましたが,誤差は少々あるもののだいたい同じ数値に収まりました.

終わりに

私の環境間違いの可能性もあるけど,少し目を疑っている.
もう少し環境を見直して,他のバージョンでも確かめてみようと思います.

LINQで書いた方が可読性高いし,認知負荷が低いからLINQで書けたら書きたいんだよな.
メソッドチェーン大好き人間としてはな….