佐々木屋

技術的なことから趣味まで色々書きます

LINQによる遅延評価

イテレータによる遅延評価と同様で、LINQでも遅延評価を実装することが出来ます。

例えば、単純に1秒待つメソッド関数があるとします。

private int testFunction(int v) {
    Thread.Sleep(1000);
    return v * 2;
}
Private Function testFunction(ByVal v As Integer) As Integer
    Thread.Sleep(1000)
    Return v * 2
End Function

LINQを利用してコレクション全ての要素をこの関数へ渡して結果を受け取るような処理を考えます。res1は先行評価、res2は遅延評価で、結果をそれぞれ2回ずつforeachでループ処理をします。違いが分かるように、Stopwatch クラスで時間を計ります。

Stopwatch sw = new Stopwatch();
int[] values = { 8, 4, 9, 5, 7 };

//先行評価
sw.Start();
var res1 = values.Select(x => testFunction(x)).ToArray();

foreach (var r in res1) {}
foreach (var r in res1) {}

sw.Stop();
Console.WriteLine("先行評価 " + sw.Elapsed);

sw.Reset();
                    
//遅延評価
sw.Start();
var res2 = values.Select(x => testFunction(x));

foreach (var r in res2) {}
foreach (var r in res2) {}

sw.Stop();
Console.WriteLine("遅延評価 " + sw.Elapsed);
Dim sw As New Stopwatch()
Dim values As Integer() = {8, 4, 9, 5, 7}

'先行評価
sw.Start()
Dim res1 = values.Select(Function(x) testFunction(x)).ToArray()

For Each r In res1
Next
For Each r In res1
Next

sw.Stop()
Console.WriteLine("先行評価 " & sw.Elapsed)

sw.Reset()
            
'遅延評価
sw.Start()
Dim res2 = values.Select(Function(x) testFunction(x))

For Each r In res2
Next
For Each r In res2
Next

sw.Stop()
Console.WriteLine("遅延評価 " & sw.Elapsed)

先行評価と遅延評価はの違いは、LINQの結果をToArrayメソッドがあるかないかの違いだけです。
どちらが何秒になるでしょうか。

先行評価 00:00:05.0092785
遅延評価 00:00:10.0103753

遅延評価が10秒と先行評価の倍かかっているのが分かります。これは、res2はLINQの段階では評価されず、foreachで初めて関数に渡されて評価される為です。これはブレークポイントを設定してみるとより分かりやすいです。

よって、foreachが2回あるので2回ずつ合計10回testFunctionが実行されます。結果、先行評価に比べて倍の時間がかかるわけです。

それに比べて先行評価は、ToArrayメソッドが実行される瞬間でLINQが評価されますので、foreachでは評価されません。つまり、5回testFunctionが実行されるだけです。


では、今度はforeachの途中で条件分岐を入れてみます。testFunctionの結果が10未満の場合はforeachを打ち切るようにします。あと、foreach文は一つに減らしてみましょう。

Stopwatch sw = new Stopwatch();
int[] values = { 8, 4, 9, 5, 7 };

//先行評価
sw.Start();
var res1 = values.Select(x => testFunction(x)).ToArray();

foreach (var r in res1) {
    if (r < 10) break;
}

sw.Stop();
Console.WriteLine("先行評価 " + sw.Elapsed);

sw.Reset();
                    
//遅延評価
sw.Start();
var res2 = values.Select(x => testFunction(x));

foreach (var r in res2) {
    if (r < 10) break;
}

sw.Stop();
Console.WriteLine("遅延評価 " + sw.Elapsed);
Dim sw As New Stopwatch()
Dim values As Integer() = {8, 4, 9, 5, 7}

'先行評価
sw.Start()
Dim res1 = values.Select(Function(x) testFunction(x)).ToArray()

For Each r In res1
    If r < 10 Then Exit For
Next

sw.Stop()
Console.WriteLine("先行評価 " & sw.Elapsed)

sw.Reset()
            
'遅延評価
sw.Start()
Dim res2 = values.Select(Function(x) testFunction(x))

For Each r In res2
    If r < 10 Then Exit For
Next

sw.Stop()
Console.WriteLine("遅延評価 " & sw.Elapsed)

さて、結果はどうなりましたか?


遅延評価のメリットとしては、

  • メモリの節約
  • 評価値を都度評価できる

という点があります。

但し、上記の例は少し極端ですが、遅延評価をよく理解しないでLINQイテレータを使用すると、思いがけないところでパフォーマンスが極端に落としてしまったり、逆にLINQを利用するメリットを無視してしまう可能性がありますので、よく理解して使用するようにしましょう。