例えば、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を使うべき。