[C#]Any関数の速度

ChatGPT大先生にとあるAny関数を使ったソースコードを添削していただくと、大体HashSetにしたほうがO(1)だから、こっちの方がいいよと仰います。

本当なのかいろいろと試してみたいと思います。

素のままの検索速度の差

順序がランダムな10000の数字の配列を用意します。その中から0を探すことを1000回行ったときの速度を計測しました。

Any関数の場合

var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
var stopwatch = Stopwatch.StartNew();
foreach(var i in Enumerable.Range(0,1000))
{
    var flag = list.Any(x => x == 0);
}
stopwatch.Stop();
Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
処理時間:43ms
処理時間:41ms
処理時間:41ms
処理時間:41ms
処理時間:42ms
処理時間:17ms
処理時間:14ms
処理時間:14ms
処理時間:13ms
処理時間:13ms

HashSetの場合

var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
var stopwatch = Stopwatch.StartNew();
foreach(var i in Enumerable.Range(0,1000))
{
    var hashSet = list.ToHashSet();
    var flag = hashSet.Contains(0);
}
stopwatch.Stop();
Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
処理時間:131ms
処理時間:121ms
処理時間:110ms
処理時間:123ms
処理時間:169ms
処理時間:114ms
処理時間:120ms
処理時間:153ms
処理時間:143ms
処理時間:134ms

個人的には既に意外ですが、まさかのAny関数の勝利でした。

毎回ToHashSetする方がパフォーマンスとしては悪いという結果になりました。

ちなみにToHashSet()をタイマーの外で行うと、さすがO(1) の実行速度です。

var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
var hashSet = list.ToHashSet();
var stopwatch = Stopwatch.StartNew();
foreach(var i in Enumerable.Range(0,1000))
{
    var flag = hashSet.Contains(0);
}
stopwatch.Stop();
Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
処理時間:0ms
処理時間:0ms
処理時間:0ms
処理時間:0ms
処理時間:0ms
処理時間:0ms
処理時間:0ms
処理時間:0ms
処理時間:0ms
処理時間:0ms

配列の中に別の配列の要素が含まれるか

とある大きい配列の中に、別で用意した配列が含まれているかのチェックをよくやることがあり、これをChatGPT先生に聞くと、HashSetを使ったほうが良いという回答をよく貰います。

文章だと分かりづらいので、例をソースコードで示すと、下記のようになります。

var list1 = [0,1,2,3,4,5,6,7,8,9];
var list2 = [2,5];
// 自分が提案した方法
var list3 = list1.Where(x => list2.Any(y => x == y));

// ChatGPT先生のリファクタリング結果
var hashSet = list2.ToHashSet();
var list4 = list1.Where(x => hashSet.Contains(x));

実際の実務で使うコードは、オブジェクトの配列となるため、これよりも複雑ですが、大雑把に言えば、このようになります。もちろん配列の大きさ、検索するオブジェクトの位置によって実行速度は大きく変わってきますが、ここでは、大量のオブジェクトの中を検索するという前提で考えていきます。

Any関数を使う場合

var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
var list2 = Enumerable.Range(0,1000);
var stopwatch = Stopwatch.StartNew();
foreach(var i in Enumerable.Range(0,1000))
{
    var flag = list.Any(x => list2.Any(y => y == x));
}
stopwatch.Stop();
Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
処理時間:182ms
処理時間:174ms
処理時間:172ms
処理時間:171ms
処理時間:181ms
処理時間:176ms
処理時間:175ms
処理時間:176ms
処理時間:181ms
処理時間:190ms

Any関数が二つあって一見、何をやってるのか非常に分かりにくいので、出来れば、これは避けたいです。

HashSetを使う場合

var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
var list2 = Enumerable.Range(0,1000);
var stopwatch = Stopwatch.StartNew();
foreach(var i in Enumerable.Range(0,1000))
{
    var hash = list2.ToHashSet();
    var flag = list.Any(x => hash.Contains(x));
}
stopwatch.Stop();
Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
処理時間:12ms
処理時間:7ms
処理時間:7ms
処理時間:7ms
処理時間:7ms
処理時間:7ms
処理時間:6ms
処理時間:6ms
処理時間:8ms
処理時間:10ms

!!

list2が大きい場合、list2をHashSetにする方が速くなりました。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です