vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】Listを検索してList(またはIEnumerable)を返したい

はじめに

この記事は C# Advent Calendar 2017 の21日目の記事です。

qiita.com

すでに何度かネタにしている Effective C# から。

第四章の29項目で、 Collection を作って返すよりも Iterator メソッドを返した方が良い、というものがあります。

理由としては遅延評価であることや、呼び出す側で都合が良いように加工しやすい、といったところがあるようです。

ただそこで気になったのがパフォーマンス。

例えばデータを保持するクラスの List があったとして、
その中の1要素だけを取ってきて for や foreach で処理したい。

この時、下記の条件で実行すると実行速度や GC Alloc のデータ量にどの程度違いがあるのでしょうか。

  1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す
  2. IEnumerable を使って Iterator メソッドとして返す
  3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用)
  4. 対象の要素だけを持った List をメソッド内で生成し、それを返す
  5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用)

  6. 1.について、 List を生成する時間は計測せず、 List を返すメソッドの実行時間のみを計測します。

感覚的には番号順通りに速い、という気がしますがどうでしょうか。

実行環境

  • Windows 10 Pro 64bit
  • Unity 2017.2.0f3

Unity を使うのは、計測に Profiler を使いたいのと、
重いデータとして 3D モデルを持たせるようにしたいためです。

データを格納するクラス

名前は特に気にしない方向で...。

Breadman.cs

using System.Collections.Generic;
using UnityEngine;

public class Breadman{
    public int Id { get; set; }
    public GameObject BreadmanModel { get; set; }
    public List SpecialMoves { get; set; }
}

こんな感じでデータを入れておきます。
また検証 1. のための List も作成します。

MainController.cs

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

public class MainController : MonoBehaviour {
    public GameObject BreadmanModel1;
    public GameObject BreadmanModel2;
    public GameObject BreadmanModel3;
    ~省略~
    private List breadmen;
    private List breadmanNames;
    
    private void Start () {

        breadmen = new List {
            new Breadman {
                Id = 0,
                BreadmanModel = BreadmanModel1,
                SpecialMoves = new List {
                    "パンチ",
                    "キック",
                }
            },
            new Breadman {
                Id = 1,
                BreadmanModel = BreadmanModel2,
                SpecialMoves = new List {
                    "空を飛ぶ",
                }
            },
            new Breadman {
                Id = 2,
                BreadmanModel = BreadmanModel3,
                SpecialMoves = new List {
                    "水に濡れる",
                }
            },
            new Breadman {
                Id = 3,
                BreadmanModel = BreadmanModel4,
                SpecialMoves = new List {
                    "力が出ない",
                }
            },
            new Breadman {
                Id = 4,
                BreadmanModel = BreadmanModel5,
                SpecialMoves = new List {
                    "新しいの",
                }
            }
        };
        // データを複製して増やす.
        var breadmen2 = new List(breadmen);
        breadmen.AddRange(breadmen2);
        var breadmen3 = new List(breadmen);
        breadmen.AddRange(breadmen3);
        var breadmen4 = new List(breadmen);
        breadmen.AddRange(breadmen4);
        var breadmen5 = new List(breadmen);
        breadmen.AddRange(breadmen5);

        breadmanNames = breadmen.Select(breadman => breadman.BreadmanModel.name).ToList();
        ~省略~
    }
    ~省略~
}

値を生成するメソッド

検証対象となる、値を生成するメソッドです。

前述の 1. ~ 5. に加えて、2. の Foreach ではなく For を使うもの(検証 2-1)、
さらにそれを逆転させたもの (検証 2-2) を追加しています。

MainController.cs

    // 検証1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す.
    private List GetBreadmanNames_CreatedList() {
        return breadmanNames;
    }
    // 検証2. IEnumerable を使って Iterator メソッドとして返す.
    private IEnumerable GetBreadmanNames_IEnumerable() {
        foreach (var breadman in breadmen) {
            yield return breadman.BreadmanModel.name;
        }
    }
    // 検証3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用).
    private IEnumerable GetBreadmanNames_IEnumerable_Linq() {
        return breadmen.Select(breadman => breadman.BreadmanModel.name);
    }
    // 検証4. 対象の要素だけを持った List をメソッド内で生成し、それを返す.
    private List GetBreadmanNames_Create() {
        var newBreadmanNames = new List();
        foreach (var breadman in breadmen) {
            newBreadmanNames.Add(breadman.BreadmanModel.name);
        }
        return newBreadmanNames;
    }
    // 検証5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用).
    private List GetBreadmanNames_Create_Linq() {
        return breadmen.Select(breadman => breadman.BreadmanModel.name).ToList();
    }
    // 検証2-1. IEnumerable を使って Iterator メソッドとして返す.
    // ForeachではなくForを使って返してみる.
    private IEnumerable GetBreadmanNames_IEnumerable_For() {
        for(var i = 0; i < breadmen.Count; i++) {
            yield return breadmen[i].BreadmanModel.name;
        }
    }
    // 検証2-2. IEnumerable を使って Iterator メソッドとして返す.
    // 順番が逆になってしまうが、速度・GC Allocを計測してみる.
    private IEnumerable GetBreadmanNames_IEnumerable_For_Reverse() {
        for(var i = breadmen.Count - 1; i >= 0; i--) {
            yield return breadmen[i].BreadmanModel.name;
        }
    }

計測する"

ボタンのクリックイベントを利用して計測してみます。

MainController.cs

~省略~
public class MainController : MonoBehaviour {
    ~省略~
    public Button Sample1Button;
    public Button Sample2Button;
    public Button Sample3Button;
    public Button Sample4Button;
    public Button Sample5Button;
    public Button Sample6Button;
    public Button Sample7Button;
    ~省略~
    private void Start() {
        ~省略~
        // 検証1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す.
        Sample1Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach(var breadmanName in GetBreadmanNames_CreatedList()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2. IEnumerable を使って Iterator メソッドとして返す.
        Sample2Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用).
        Sample3Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_Linq()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証4. 対象の要素だけを持った List をメソッド内で生成し、それを返す.
        Sample4Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_Create()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用).
        Sample5Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_Create_Linq()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2-1. IEnumerable を使って Iterator メソッドとして返す(For使用).
        Sample6Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_For()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2-2. IEnumerable を使って Iterator メソッドとして返す(Forを逆転させて使用).
        Sample7Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_For_Reverse()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
    }
    ~省略~

起動 -> ボタンを5回押す -> Profiler の結果を見る 、という内容で確認してみました。

結果

結果は下記の通り。

f:id:mslGt:20171224115042j:plain

やっぱりというか、あらかじめ作成済みの List を返すのが速いようです。

ただ、どのケースを見ても GC Alloc のデータ量は変わらなかったのは少し驚きました。
( List を生成する方がデータ量が多そうだったので)

また、 Foreach や For を使った場合と Linq を使う場合とを比較すると、
最初の一回は Foreach や For を使うほうが速く、それ以降は Linq の方が速い傾向がありました。

ただ速度に関しては、このあと何度か試すと必ずしも上記の通りとはならなかったので、
(検証1. も他と同じくらいの速度になったり)
どの方法を採るか、というのはもう少し検証が必要そうです。

おわりに

少なくとも今回の内容からすると、検証1. の List として持たせるのが速度を最優先するなら良さそうです。

ただ、それぞれを List に、とかし始めるとバグの温床にもなりそうですし、
そもそもデータを格納するためのクラスを作る意味もなくなってくるという。。。

またキャッシュしておくデータの量も増えてしまいますね。

メソッド内で生成する検証2. ~ 検証7. を見ると、ほとんど変わらないように思います。

ということで、検証2. か検証3. の方法で IEnumerable を返して、
呼び出し元で都合が良いように加工する、というのが良さそうです。

やっぱり実際測ってみると意外な結果が見えたりして面白いですね。

最後に、検証のネタに使った Breadman を載せておきます。

www.instagram.com

参照