[c#]FindAllが遅かったので消して高速化してみた

2021-03-08

例えば、30万件のレコードから15レコード抽出する処理を1万回繰り返すと何秒かかるか実験したことはありますか?

FindAllの処理速度を計測してみる

FindAllを使用した場合、正解は42秒です。ちょっと時間がかかりますね。
1回の処理にかかる時間は5ミリ秒以下なのですが、それを1万回なので塵積って山となるの典型例な状態。
もっと大量のデータを扱う場合はさらに遅くなります。嫌になりますね…。

今回はそんなFindAllを大量にループしようとして処理が遅くなってしまっているソースコードを劇的ビフォーアフターして処理を驚きの速さに改善していきます!

using System;
using System.Collections.Generic;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            // データ登録部分
            // 検索対象のユーザ一覧
            var wannaOutputList = new List<User>();
            // 全レコード情報
            var allList = new List<RecordJouhou>();
            // ユーザ人数
            var allRecordCount = 20000;
            // 一人当たりが持っている情報の数
            var kojinJouhouCount = 15;

            //検索対象は10000人
            for (var i = 0; i < allRecordCount; i += 2)
            {
                wannaOutputList.Add(new User(i));
            }
            // 20000人×15情報のレコードを登録
            for (var j = 0; j < kojinJouhouCount; j++)
            {
                for (var i = 0; i < allRecordCount; i++)
                {
                    allList.Add(new RecordJouhou(i, j));
                }
            }

            // 抽出したレコード情報を格納するリスト
            List<List<RecordJouhou>> outputList = new List<List<RecordJouhou>>();

            // 時間計測開始
            var start = DateTime.Now;

            foreach (var wannaOutputRecod in wannaOutputList)
            {
                outputList.Add(allList.FindAll(x => x.Name.Equals(wannaOutputRecod.Name)));
            }

            // 時間計測終了
            var end = DateTime.Now;

            var elasperTime = end - start;
            Console.WriteLine($"処理終了までにかかった時間 {elasperTime}");
            Console.WriteLine($"出力するユーザ数 {outputList.Count}");
            Console.WriteLine($"出力するレコードの総数 {outputList.Count * kojinJouhouCount}");
            Console.WriteLine($"1ユーザ抽出にかかった時間(ミリ秒) {elasperTime.TotalMilliseconds / outputList.Count}");
            Console.ReadLine();
        }
        public class User
        {
            public User(int i)
            {
                Name = $"User{i}";
            }
            public string Name { get; set; }
        }

        public class RecordJouhou
        {
            public RecordJouhou(int i, int j)
            {
                Name = $"User{i}";
                KojinJouhou = $"{Name}の{j}個目のKojinJoubou";
            }

            public string Name { get; set; }
            public string KojinJouhou { get; set; }

        }
    }
}

Findに処理を変えてみる

世間一般的には「Findは早い」と言われています。
実際にどのくらい早いのか先ほどのコードを少しだけ変更して、確かめてみましょう。

            // 抽出したレコード情報を格納するリスト
            List<List<RecordJouhou>> outputList = new List<List<RecordJouhou>>();

            // 時間計測開始
            var start = DateTime.Now;

            foreach (var wannaOutputRecod in wannaOutputList)
            {
                outputList.Add(new List<RecordJouhou> { allList.Find(x => x.Equals(wannaOutputRecod)) });
            }

            // 時間計測終了
            var end = DateTime.Now;

34~46行目のコードを上記のようにFindにかえてみました。1レコード目しか抽出できないですが、これで早くなるはず。

結果は1.47秒です。FindAllとは全くスピードが違うことが分かります。

管理人代理
管理人代理

むちゃくちゃ早い!

FindAllの代わりにFindを使って何とか処理ができないのか

上司
上司

けど、1行しか探せないならFindの方が早くても
FindAllの代わりにならないよね。

管理人代理
管理人代理

そうなんですよね……。

FindとFindAllがなぜこんなにも処理時間が変わるのかというと、Findは検索対象を見つけたら次のリスト処理ができるのに対して、FindAllは最後まで探さないといけないからです。

結局検索しないといけない回数が増えると、その分だけ処理が遅くなってしまいます。
ということでこのFind1回だけでFindAllで検索できる全てのレコードを見つけ出せるように、処理を改造すれば処理時間は短縮できるはずです。

            // 抽出したレコード情報を格納するリスト
            List<List<RecordJouhou>> outputList = new List<List<RecordJouhou>>();

            // 時間計測開始
            var start = DateTime.Now;

            var allListGroupBy = allList.GroupBy(x => x.Name).ToList();

            foreach (var wannaOutputRecod in wannaOutputList)
            {
                outputList.Add(allListGroupBy.Find(x=>x.Key.Equals(wannaOutputRecod.Name)).ToList());
            }

            // 時間計測終了
            var end = DateTime.Now;

GroupByを使用して、ユーザ毎にまとめてからFindをします。
この方法であればFind1回で15レコード全て取得できるのでFindAllを使わなくてもよくなります。

結果は1.69秒に。
Findに近い処理時間で実行できるようになりました!

実行結果も想定通り格納されていることを確認。

まとめ

ループの中でFindAllをするより断然GroupByしてからFindを使うべき。