はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity,C#】foreachがfor文に処理速度で勝つとき

はじめに

今回はforeachとfor文についての記事を書いていきたいと思います。

for文とforeachにはそれぞれ特徴があり、使い分けが必要ですよね。

確か私はfor文は高速だけど、foreachは可読性が高いといった教え方をされたような気がします。

ただふと疑問に思ったのです、「本当にfor文の方が高速なのか?」と。

というわけで実験をしていきます。

環境

Unity2019.2.11f1
※普段UnityでC#を使うので、そちらでのパフォーマンスです。

前提

最初にややネタバレちっくですが、for文とforeachの機能はまったく違います

・for文:ある条件が成立するまで繰り返す

・foreach文:ある列挙インターフェイスが列挙する要素を繰り返して1つ1つ取得する

foreachIEnumerableインターフェイス(厳密にはGetEnumeratorメソッド)を実装していなければなりません。

そしてGetEnumeratorメソッドの返り値はEnumerator構造体(IEnumerator<T>IEnumeratorを実装)なので、MoveNextメソッドとCurrentプロパティを実装、つまりは次のデータと現在のデータしか保持していないのです。(IDisposableResetメソッドもありますが省略します)

実験①

まずは普通にシンプルな勝負をしましょう。

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>ですね。スタックキューなんかもそうでしょうか。

これを要素番号を指定するとなると、LinqElementAt(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文の方が早い場合が多いと思います。

ただ、やはり可読性などの面からもforforeachをうまく使い分けていきたいですね!