【UnityC#】forループとforeachループの処理時間を比較してみた

# はじめに
Unityでのforループとforeachループの配列アクセスの処理時間を計測・比較してみました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/f925ec4a-11c3-7d3d-afe0-7308650d4f3c.png)

# 環境
Excel2016
Windows 10
Unity 2017.3.0f3

# PCスペック
|||
|:--|:--|
|メモリ|32GB|
|CPU|Intel(R) Core(TM) i7-6800K CPU @ 3.40GHz|

# 今回やったこと
手順1. forループとforeachループの処理時間を**500回計測**し、CSVに保存
手順2. CSVから処理時間の**ヒストグラムを作成**
手順3. ヒストグラムから**グラフを作成**・比較

# 計測コード
今回は以下の二つのコードで処理時間を計測します。

```csharp:forループを使った配列走査
int x;
for (int i = 0; i < N; i++)
{
    x = a[i];
}
```

```csharp:foreachループを使った配列アクセス
int x;
foreach (var item in a)
{
    x = item;
}
```

今回は配列aの長さを10万としました(N = 100000)

# 計測

## CSVの作成を行うクラス FileManager

```FileManager.cs
using UnityEngine;
using System.IO;
using System;

public class FileManager
{
    // ファイル書き出し
    public static void Save(string text)
    {
#if UNITY_EDITOR
        var now = System.DateTime.Now;
        var datetime = string.Format("{0}{1:D2}{2:D2}_{3:D2}{4:D2}{5:D2}", now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
        var path = Application.dataPath + "/" + string.Format("Measure{0}.csv", datetime);

        System.IO.StreamWriter sw = new System.IO.StreamWriter(path, false, System.Text.Encoding.GetEncoding("shift_jis"));

        sw.Write(text);
        sw.Flush();
        sw.Close();

        UnityEditor.AssetDatabase.Refresh();
#endif
    }
}
```

## 処理時間を計測するクラス MeasureTest 

計測を行うメインのクラスはこちらです。

```MeasureTest.cs
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using UnityEngine;

/// <summary>
/// 処理負荷の計測テスト
/// </summary>
public class MeasureTest : MonoBehaviour
{
    const int COUNT = 500; // 計測回数
    const int N = 100000; // 配列の長さ
    int[] a = new int[N]; // 何らかの配列


    void Start()
    {
        string text = "";
        text += string.Format("計測日時 : {0}\n", System.DateTime.Now);
        text += string.Format("計測回数 : {0}\n", COUNT);
        text += string.Format("配列アクセス回数 : {0}\n", N);
        text += string.Format("For(ms),ForEach(ms)\n");

        for (int i = 0; i < COUNT; i++)
        {
            double forSec = TestFor();
            double foreachSec = TestForEach();

            text += string.Format("{0},{1}\n", forSec, foreachSec);
        }

        // 計測結果は外部ファイルへ
        FileManager.Save(text);
    }

    // forループの処理時間の計測
    double TestFor()
    {
        int x;
        var sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < N; i++)
        {
            x = a[i];
        }
        sw.Stop();

        return 1000.0 * (double)sw.ElapsedTicks / (double)Stopwatch.Frequency; // 処理時間
    }

    // foreachの処理時間の計測
    double TestForEach()
    {
        int x;
        var sw = new Stopwatch();
        sw.Start();
        foreach (var item in a)
        {
            x = item;
        }
        sw.Stop();

        return 1000.0 * (double)sw.ElapsedTicks / (double)Stopwatch.Frequency; // 処理時間
    }
}
```

## 計測する
上記のMeasureTestクラスをシーン内の適当なGameObjectにアタッチしてゲームを再生します。

ゲームを再生するとMeasure_(日付).csvという名前のファイルが作成されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/a9cfa9f2-f40b-5017-9e36-104fc7e7dfa4.png)

## CSVの中身
CSVの中身はこのような感じになっています。

<img src ="https://qiita-image-store.s3.amazonaws.com/0/44288/24bd9d86-5a78-47b7-b69a-e9dbb8c5b2a2.png" width = 320>


# 手順2. ヒストグラムを作成する
続いては、このCSVからヒストグラムを作っていきたいと思います。

## CSVの編集 : データ区間の定義
まずは、データ区間となる数値を入力していきます。

CSVを以下のように編集し、**データ区間を定義します。**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/d54cbb60-3172-c1f2-d4c8-2df32c56fb06.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/b10d9e92-ae08-26a8-67a6-abc8e5aa2186.png)



## ヒストグラムの作成 
参考 : <a href = "https://udemy.benesse.co.jp/office-enhance/excel-histogram.html">エクセルでヒストグラムを作成する方法。正しいグラフの分析とは？</a>


### forループの処理時間のヒストグラム作成
まずは"For(ms)"と"データ区間"に関してのヒストグラムを作成します。

ヒストグラム設定
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/b5985a29-d5f8-6c2e-18ab-04890f90eded.png)

作成直後
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/9a16a10a-ce76-5524-8681-3cf38b9ee341.png)

### foreachループの処理時間のヒストグラム作成
次に"ForEach(ms)"と"データ区間"に関してのヒストグラムを作成します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/12e5efca-de26-ef47-28f1-d1d7acddffb6.png)

作成直後
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/e4e3a66a-04d4-6e6f-3d02-64ce5f90dffd.png)

### CSVを整形する
余分な行が入っていたり、名前があいまいなので見やすいように整形します。

E列のカラム名 "頻度" -> "for(ms)"
G列のカラム名 "頻度" -> "foreach(ms)"
D列とF列を削除

![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/943854e8-7ed9-e1a4-3be1-a479a8629211.png)


# 手順3. ヒストグラムからグラフを作成する

## 数値を選択する
"データ区間"、"for(ms)"、"foreach(ms)"の数値を選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/104d4621-0351-e089-dda0-8bb9114523e1.png)

## グラフの作成
おすすめグラフから"集合縦棒"を選択します。
<img src ="https://qiita-image-store.s3.amazonaws.com/0/44288/b0dfafb2-04e0-957d-68bb-e246ff21d9a9.png" width = 320>

作成直後
<img src = "https://qiita-image-store.s3.amazonaws.com/0/44288/c55caae0-8efe-95e5-c2bf-ccd5926db145.png" width = 320>


## グラフの整理
分かりやすいように整理します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/f925ec4a-11c3-7d3d-afe0-7308650d4f3c.png)


## グラフの考察
・**foreachは若干重い** (forのほうがちょっと軽い)
・グラフのピークが両方とも**左側へ寄っている**。　どういうことなんだろう？
・forの処理時間 > foreachの処理時間となってしまうケースも低確率で起こり得る。

## 平均値も比較
処理時間の平均値も比較してみました。<br>

forループを処理時間の平均値は **0.7358128(ms)**
foreachループの処理時間の平均値は **0.8249308(ms)**

0.8249308 / 0.7358128 ≒ 1.121115044

foreach の処理時間は　for と比較して　**12％**高くなるようです。

# 結論
foreach は for より ちょっと重い 

# 追記: List\<T>の処理時間
ちょうどよい機会なので、forやforeachでList\<T>にアクセスした場合の処理負荷時間も計測してみました。

## N = 1000の場合
リストや配列の要素数 : 1000
計測回数 : 500

ヒストグラム
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/b34258a8-182f-90c9-5f20-4e6d94484e05.png)

平均値などの比較
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/0fbcd7f6-977b-f0ef-03dc-b6970ddb63ba.png)


foreachでListにアクセスした場合、forループ配列アクセスの**約５倍**の処理時間がかかるようです (要素数が**1000**の場合)

## N = 10万の場合
リストや配列の要素数 : 10万
計測回数 : 500

ヒストグラム
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/43f7f1ad-0236-e572-c6d2-43ce254b7cdc.png)

平均値などの比較
![image.png](https://qiita-image-store.s3.amazonaws.com/0/44288/a81464d1-1095-1e91-3fdd-35eff5a47720.png)

foreachでListにアクセスした場合、forループ配列アクセスの**約５倍**の処理時間がかかるみたいです (要素数が**10万**の場合)

## List\<T>の計測ソースコード
List\<T>の計測に使用したコードは以下になります。

```MeasureFor.cs
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using UnityEngine;

/// <summary>
/// forループやforeachループの処理負荷の計測テスト
/// </summary>
public class MeasureFor : MonoBehaviour
{
    const int COUNT = 500; // 計測回数
    const int N = 1000; // 配列の長さ
    int[] array = new int[N]; // 何かの配列
    List<int> list = new int[N].ToList(); // リスト

    void Start()
    {
        string text = "";
        text += string.Format("計測日時 : {0}\n", System.DateTime.Now);
        text += string.Format("Unityバージョン : {0}\n", Application.unityVersion);
        text += string.Format("計測回数 : {0}\n", COUNT);
        text += string.Format("配列アクセス回数 : {0}\n", N);
        text += string.Format("For-Array(ms),ForEach-Array(ms),For-List(ms),ForEach-List(ms)\n");

        for (int i = 0; i < COUNT; i++)
        {

            text += string.Format("{0},{1},{2},{3}\n", TestForArray(), TestForEachArray(), TestForList(), TestForEachList());
        }

        // 計測結果は外部ファイルへ
        FileManager.Save(text);
    }

    // forループの処理時間の計測
    double TestForArray()
    {
        int x;
        var sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < N; i++)
        {
            x = array[i];
        }
        sw.Stop();

        return 1000.0 * (double)sw.ElapsedTicks / (double)Stopwatch.Frequency; // 処理時間
    }

    // foreachの処理時間の計測
    double TestForEachArray()
    {
        int x;
        var sw = new Stopwatch();
        sw.Start();
        foreach (var item in array)
        {
            x = item;
        }
        sw.Stop();

        return 1000.0 * (double)sw.ElapsedTicks / (double)Stopwatch.Frequency; // 処理時間
    }
    
    // forループの処理時間の計測
    double TestForList()
    {
        int x;
        var sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < N; i++)
        {
            x = list[i];
        }
        sw.Stop();

        return 1000.0 * (double)sw.ElapsedTicks / (double)Stopwatch.Frequency; // 処理時間
    }

    // foreachの処理時間の計測
    double TestForEachList()
    {
        int x;
        var sw = new Stopwatch();
        sw.Start();
        foreach (var item in list)
        {
            x = item;
        }
        sw.Stop();

        return 1000.0 * (double)sw.ElapsedTicks / (double)Stopwatch.Frequency; // 処理時間
    }
}
```