2009/11/04

IList<T>の走査処理の比較(インデクサ/GetEnumerator/foreach)

twitterで「IList<t>を走査する場合でforとforeachとで意外に差が出た」とか書いたら「IListの内部実装にもよりますけど、結構変わったりしますよねー」とRTがあった。
それで処理速度を計測するコードを書いてみたらよく分からない結果になった。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace IListEnumerationBenchmark
{
    class Program
    {
        static void Main(string[] args)
        {
            var size = 1000000;
            var count = 10;
            var sw = new Stopwatch();

            Action<IList<int>> run = list =>
            {
                var useIndexerTicks = 0L;
                var useEnumeratorTicks = 0L;
                var useForeachTicks = 0L;

                for (var c = 0; c < count; c++)
                {
                    var random = new Random(c);
                    for (var n = 0; n < list.Count; n++)
                        list[n] = random.Next(int.MinValue, int.MaxValue);

                    // use indexer
                    sw.Reset();
                    sw.Start();
                    var useIndexerResult = UseIndexer(list);
                    sw.Stop();
                    useIndexerTicks += sw.ElapsedTicks;

                    // use enumerator
                    sw.Reset();
                    sw.Start();
                    var useEnumeratorResult = UseEnumerator(list);
                    sw.Stop();
                    useEnumeratorTicks += sw.ElapsedTicks;

                    // use foreach
                    sw.Reset();
                    sw.Start();
                    var useForeachResult = UseForeach(list);
                    sw.Stop();
                    useForeachTicks += sw.ElapsedTicks;

                    // correctness
                    if (useIndexerResult != useForeachResult)
                        Console.WriteLine("useIndexerResult: {0}, useForeachResult: {1}", useIndexerResult, useForeachResult);
                    if (useEnumeratorResult != useForeachResult)
                        Console.WriteLine("useEnumeratorResult: {0}, useForeachResult: {1}", useEnumeratorResult, useForeachResult);
                }

                Console.WriteLine("use indexer: {0} ({1:F})", useIndexerTicks / count, useIndexerTicks / (double)useForeachTicks);
                Console.WriteLine("use enumerator: {0} ({1:F})", useEnumeratorTicks / count, useEnumeratorTicks / (double)useForeachTicks);
                Console.WriteLine("use foreach: {0} ({1:F})", useForeachTicks / count, useForeachTicks / (double)useForeachTicks);
                Console.WriteLine();
            };

            // int[]
            {
                Console.WriteLine("int[]: ");
                var array = new int[size];
                run(array);
            }

            // List<int>
            {
                Console.WriteLine("List<int>: ");
                var list = new List<int>();
                for (var n = 0; n < size; n++)
                    list.Add(0);
                run(list);
            }
        }

        static int UseIndexer(IList<int> list)
        {
            var result = 0;
            for (var n = 0; n < list.Count; n++)
                result = unchecked(result + list[n]);
            return result;
        }

        static int UseEnumerator(IList<int> list)
        {
            var result = 0;
            var e = list.GetEnumerator();
            while (e.MoveNext())
                result = unchecked(result + e.Current);
            return result;
        }

        static int UseForeach(IList<int> list)
        {
            var result = 0;
            foreach (var x in list)
                result = unchecked(result + x);
            return result;
        }
    }
}
ksksts / junk / source — bitbucket.org

結果は次の通り。
配列ではインデクサでのアクセスがforeachより遅いのにList<t>では逆になるのが意味が分からない。あと両方でGetEnumeratorでのアクセスがforeachよりも速いのも意味が分からない(foreachの方が最適化する余地があるんじゃないかと思う)。
こんな処理にフォーカスした速度なんかほとんどのケースで無視していいだろうから、インデクサでのアクセスが必要じゃない処理では常にforeachを使うけど。
書きながらList<t>.ForEachを思い出した(IList<t>には無い)。使える場合ならforeachよりもそれを使うと思う。ILとかコンパイル後のコードはたぶん見ない。
int[]:
use indexer: 578500 (5.57)
use enumerator: 87133 (0.84)
use foreach: 103895 (1.00)

List<int>:
use indexer: 132922 (0.76)
use enumerator: 154705 (0.89)
use foreach: 174277 (1.00)
追記: 上の結果はIList<T>を通した結果だから不正確。ksksts's blog: 配列やList<T>をIList<T>型のインデクサでアクセスするとパフォーマンスが悪い(don't use IList<T>'s indexer)を参照。

0 件のコメント: