はじめに
Unityでのforループとforeachループの配列アクセスの処理時間を計測・比較してみました。
環境
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. ヒストグラムからグラフを作成・比較
計測コード
今回は以下の二つのコードで処理時間を計測します。
int x;
for (int i = 0; i < N; i++)
{
x = a[i];
}
int x;
foreach (var item in a)
{
x = item;
}
今回は配列aの長さを10万としました(N = 100000)
計測
CSVの作成を行うクラス FileManager
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
計測を行うメインのクラスはこちらです。
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という名前のファイルが作成されます。
CSVの中身
CSVの中身はこのような感じになっています。
手順2. ヒストグラムを作成する
続いては、このCSVからヒストグラムを作っていきたいと思います。
CSVの編集 : データ区間の定義
まずは、データ区間となる数値を入力していきます。
ヒストグラムの作成
参考 : エクセルでヒストグラムを作成する方法。正しいグラフの分析とは?
forループの処理時間のヒストグラム作成
まずは"For(ms)"と"データ区間"に関してのヒストグラムを作成します。
foreachループの処理時間のヒストグラム作成
次に"ForEach(ms)"と"データ区間"に関してのヒストグラムを作成します。
CSVを整形する
余分な行が入っていたり、名前があいまいなので見やすいように整形します。
E列のカラム名 "頻度" -> "for(ms)"
G列のカラム名 "頻度" -> "foreach(ms)"
D列とF列を削除
手順3. ヒストグラムからグラフを作成する
数値を選択する
"データ区間"、"for(ms)"、"foreach(ms)"の数値を選択します。
グラフの作成
グラフの整理
グラフの考察
・foreachは若干重い (forのほうがちょっと軽い)
・グラフのピークが両方とも左側へ寄っている。 どういうことなんだろう?
・forの処理時間 > foreachの処理時間となってしまうケースも低確率で起こり得る。
平均値も比較
処理時間の平均値も比較してみました。
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
foreachでListにアクセスした場合、forループ配列アクセスの約5倍の処理時間がかかるようです (要素数が1000の場合)
N = 10万の場合
リストや配列の要素数 : 10万
計測回数 : 500
foreachでListにアクセスした場合、forループ配列アクセスの約5倍の処理時間がかかるみたいです (要素数が10万の場合)
List<T>の計測ソースコード
List<T>の計測に使用したコードは以下になります。
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; // 処理時間
}
}