まめ - たんたんめん

備忘録 C# / WPF 多め

C# List<T>のforeachループの最速を検証してみる

元ネタはこちらのブログです。 (C#) List<T>からSpan<T>を引き抜いて高速化 - ネコのために鐘は鳴る

知らなかったのですがListのforeachはパフォーマンスが悪いみたいですね。 今回はブログに書いてある手法のパフォーマンスを実測して検証してみようと思います。

今回のテストでは 0~5000の値をリストに入れて、それらの合計を求めるプログラムでパフォーマンスを検証してみます。

検証環境 今回もBenchmarkDotnet / Release , デバッガアタッチ無しです。

リストの定義

        private List<int> _list;
        [GlobalSetup]
        public void Initialize()
        {
            _list = new List<int>(Enumerable.Range(0,5000));
        }

まずは普通にforeachを使って回すパターン。一番ありがちなやつですね。

        [Benchmark]
        public int List()
        {
            int result = 0;
            foreach (var item in _list)
            {
                result += item;
            }

            return result;
        }

結果は11.506 usでした。


次にAsSpanです。 参照元のブログに書かれていますが、Unsafeを使ってTを強引に取り出すという方法です。
注意しないといけないのはIListではなんでも使えるのではなく、Listのようにデータの先頭にT
を保持するようなケースでしか利用できません。

以下、ListのT[]を取り出してSpanに変換します。

    public static class ListEx
    {
        private class list_internal<T>
        {
            internal T[] items;
        }
        public static Span<T> AsSpan<T>(this List<T> list)
        {
            return Unsafe.As<list_internal<T>>(list).items.AsSpan(0, list.Count);
        }
    }

そして同じようにループを回します。

        [Benchmark]
        public int Span()
        {
            int result = 0;
            foreach (var item in _list.AsSpan())
            {
                result += item;
            }

            return result;
        }

結果は...2.529 us !!なんと5倍程度早くなっていることが分かりました。
これはすごいですね。ただ、Unsafe.Asを使うのは今後とも100%安全とは保障できないので必要に応じて自己責任で使うという感じでしょうか。


おまけで、ToArray()での検証もしてみました。
Listのイテレートが遅いのであれば一度T[]に変換してあげるだけでも早くなるのでは?

            int result = 0;
            foreach (var item in _list.ToArray())
            {
                result += item;
            }

            return result;

計測するまでは変換コストが高いからそのまま回すほうが早いだろうと思っていたのですが...
結果はなんと...5.023 us素数が5000程度であればListのまま回すよりは2倍速くなりました!
これは予想外だったのでびっくりしました。ToArray()であれば、普通に使えそうですね。

今回の計測結果をまとめます。

f:id:at12k313:20201217133424p:plain