はじめに
今回はforeachとfor文についての記事を書いていきたいと思います。
for文とforeachにはそれぞれ特徴があり、使い分けが必要ですよね。
確か私はfor文は高速だけど、foreachは可読性が高いといった教え方をされたような気がします。
ただふと疑問に思ったのです、「本当にfor文の方が高速なのか?」と。
というわけで実験をしていきます。
環境
Unity2019.2.11f1
※普段UnityでC#を使うので、そちらでのパフォーマンスです。
前提
最初にややネタバレちっくですが、for文とforeachの機能はまったく違います。
・for文:ある条件が成立するまで繰り返す
・foreach文:ある列挙インターフェイスが列挙する要素を繰り返して1つ1つ取得する
foreachはIEnumerableインターフェイス(厳密にはGetEnumeratorメソッド)を実装していなければなりません。
そしてGetEnumeratorメソッドの返り値はEnumerator構造体(IEnumerator<T>とIEnumeratorを実装)なので、MoveNextメソッドとCurrentプロパティを実装、つまりは次のデータと現在のデータしか保持していないのです。(IDisposable,Resetメソッドもありますが省略します)
実験①
まずは普通にシンプルな勝負をしましょう。
using System.Collections.Generic; using UnityEngine; using System.Linq; public class Test : MonoBehaviour { private void Start() { List<int> target = Enumerable.Range(0, 10000000).ToList(); int sum = 0; System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); for(int i = 0; i < target.Count; i++) { sum += target[i]; } sw.Stop(); Debug.Log($"for: { sw.ElapsedMilliseconds }ms"); sw.Reset(); sum = 0; sw.Start(); foreach(var item in target) { sum += item; } sw.Stop(); Debug.Log($"foreach: { sw.ElapsedMilliseconds }ms"); } }
結果は…
| 名前 | 処理時間(ms) |
|---|---|
| for | 485 |
| foreach | 528 |
さすがfor文、foreachよりも早い速度を出してきました。
foreachが勝つとき
ただしランダムアクセス性がない(シーケンシャルアクセス)コレクションのときは結果が変わります。
ランダムアクセス性がないとは、numbers[1]みたく普通には要素番号を指定して要素を取得したりできないことです。例えばIEnumerable<T>ですね。
これを要素番号を指定するとなると、LinqのElementAt(IEnumerableの拡張メソッド)を使うしかないでしょう。
using UnityEngine; using System.Linq; public class Test : MonoBehaviour { private void Start() { var numbers = Enumerable.Range(0, 10000000); int sum = 0; System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); for(int i = 0; i < numbers.Count(); i++) { sum += numbers.ElementAt(i); } sw.Stop(); Debug.Log($"for: { sw.ElapsedMilliseconds }ms {sum}"); sw.Reset(); sum = 0; sw.Start(); foreach(var item in numbers) { sum += item; } sw.Stop(); Debug.Log($"foreach: { sw.ElapsedMilliseconds }ms {sum}"); } }
| 名前 | 処理時間(ms) |
|---|---|
| for | 1578 |
| foreach | 539 |
foreachは先程とほぼ変わりませんが、for文は一気に遅くなってしまいましたね。
一応以下のようにコードを変更すれば少し早くなりますが、やはりforeachには勝てません。
int count = numbers.Count(); for(int i = 0; i < count; i++) { sum += numbers.ElementAt(i); }
| 名前 | 処理時間(ms) |
|---|---|
| for | 1220 |
| foreach | 530 |
for文を魔改造
ただし、さらに工夫を重ねればforeachと並ぶことができます。
using UnityEngine; using System.Linq; public class Test : MonoBehaviour { private void Start() { var numbers = Enumerable.Range(0, 10000000); int sum = 0; System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); var enumerator = numbers.GetEnumerator(); for(;enumerator.MoveNext();) { sum += enumerator.Current; } sw.Stop(); Debug.Log($"for: { sw.ElapsedMilliseconds }ms {sum}"); sw.Reset(); sum = 0; sw.Start(); foreach(var item in numbers) { sum += item; } sw.Stop(); Debug.Log($"foreach: { sw.ElapsedMilliseconds }ms {sum}"); } }
| 名前 | 処理時間(ms) |
|---|---|
| for | 576 |
| foreach | 547 |
ほとんど同じ値にまで近づけることができました。
ただこのfor文の中身は、もはやforeachの内部的な仕組みとほぼ同じになっています。
わざわざこんなコードを書くなら、foreachを書いたほうがよいでしょう。
さいごに
少しはforeachの名誉を守りましたが、速度的にはfor文の方が早い場合が多いと思います。
ただ、やはり可読性などの面からもfor,foreachをうまく使い分けていきたいですね!