遅延評価
遅延評価 ▲
引数や変数の値が必要になるまで評価を行わない(遅延させる)方法を遅延評価という
基本的にクエリなどに対しては遅延評価が使用されるケースが多い
遅延評価によって『省メモリ』『走査時点でのデータで結果が得られる』『無限ループへの対応ができる』といったメリットがある
遅延評価に関して具体的にどんな処理が行われているか以下に例を示す(参考文献1をもとに記載)
using System;
using System.Collections.Generic;
class Program
{
static int HeavyFunction( int p )
{
System.Threading.Thread.Sleep( 100 );
return p * p;
}
static void Main( string[] args )
{
int[] x = { -15, -10, -5, 0, 5, 10, 15 };
int min = -10;
int max = 10;
DateTime t = DateTime.Now;
// この時点では評価されない(遅延させている)
var y =
from p in x
where min <= p && p <= max
select HeavyFunction( p );
TimeSpan ts = DateTime.Now - t;
Console.Write( "{0}\n", ts.Ticks );
t = DateTime.Now;
// foreach で値が必要とされてはじめて計算される
foreach ( var p in y )
{
ts = DateTime.Now - t;
Console.Write( "{0}: {1}\n", ts.Ticks, p );
t = DateTime.Now;
}
}
}
// where や select をわかりやすいように上書き拡張(実際LINQも似たような処理を行ってるはず)
public static class Extensions
{
public static IEnumerable<int> Where( this IEnumerable<int> x, Func<int, bool> f )
{
foreach ( int p in x )
{
if ( f( p ) )
{
yield return p;
}
}
}
public static IEnumerable<int> Select( this IEnumerable<int> x, Func<int, int> f )
{
foreach ( int p in x )
{
yield return f( p );
}
}
}
// 結果
// 48471 最初のクエリ文では時間がかかっていない
// 1196706: 100 きちんと5等分に負荷が分散されている
// 1091368: 25
// 1115735: 0
// 1106152: 25
// 1091472: 100
これに対し、クエリ文生成の時点で即時評価する場合のソースコードは以下のようになる
// where や select を上書き拡張(即時評価版)
public static class Extensions
{
public static IList Where( this IList x, Func f )
{
List y = new List();
for ( int i = 0; i < x.Count; ++i )
{
if ( f( x[ i ] ) )
{
y.Add( x[ i ] );
}
}
return y;
}
public static IList Select( this IList x, Func f )
{
List y = new List();
for ( int i = 0; i < x.Count; ++i )
{
y.Add( f( x[ i ] ) );
}
return y;
}
}
// 上記置き換えた場合の結果
// 5634020 即時評価なので最初に時間がかかる
// 177: 100 以降は時間がかからない
// 21: 25
// 1: 0
// 1: 25
// 1: 100
即時評価 ▲
遅延評価に対してある時点における結果をすぐに受け取る方法を即時評価という
即時評価は『結果を即座に作成してキャッシュできる』メリットがある
そのためケースによっては即時評価が実行速度向上に寄与することもある
以下にその例を記す
using System;
using System.Collections.Generic;
class Program
{
static void Main( string[] args )
{
// Test クラスを 10000個生成する
int num = 10000;
List<Test> list = new List<Test>();
for ( int i = 0; i < num; i++ )
{
list.Add( new Test() { Value = i } );
}
int compareLine = 5000;
int target = 7777;
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
// 遅延評価①
sw.Start();
IEnumerable<Test> delay = list.Where( x => x.Value > compareLine );
for ( int index = 0; index < num; ++index )
{
Test result1 = delay.First( x => x.Value == target );
}
sw.Stop();
Console.WriteLine( sw.Elapsed );
sw.Reset();
// 即時評価①
sw.Start();
List<Test> immediate = list.Where( x => x.Value > compareLine ).ToList();
for ( int index = 0; index < num; ++index )
{
Test result2 = immediate.First( x => x.Value == target );
}
sw.Stop();
Console.WriteLine( sw.Elapsed );
sw.Reset();
// 結果①
// 00:00:00.7477895 遅延評価
// 00:00:00.2391093 即時評価
// 遅延評価のほうは First が呼ばれたタイミングで Where の評価を毎回行っているので遅くなる
// 即時評価は ToList が呼ばれたタイミングで Where の処理が終わり、First が呼ばれたタイミングではただ結果を返すだけなので速くなっている
// メモリ効率の点では遅延評価のほうが Where の結果のリストを作成していない分効率が良い
// 遅延評価②
sw.Start();
IEnumerable<Test> delay2 = list.Where( x => x.Value > compareLine );
if ( delay2.Count() > 0 )
{
foreach ( var item in delay2 )
{
int a = item.Value;
}
}
sw.Stop();
Console.WriteLine( sw.Elapsed );
sw.Reset();
// 即時評価②
sw.Start();
List<Test> immediate2 = list.Where( x => x.Value > compareLine ).ToList();
if ( immediate2.Count > 0 )
{
foreach ( var item in immediate2 )
{
var a = item.Value;
}
}
sw.Stop();
Console.WriteLine( sw.Elapsed );
sw.Reset();
// 結果②
// 00:00:00.0003030 遅延評価
// 00:00:00.0001888 即時評価
// 遅延評価のほうは Count が呼ばれたタイミングで Where の評価が行われており、さらに foreach のタイミングでも同様に評価が行われている
// 即時評価のほうは Where の評価は ToList のタイミングで終わっており、そのキャッシュリストから Count や foreach の処理を行っているため速くなっている
}
}
class Test
{
public int Value { get; set; }
}
目次