yieldのパフォーマンス(C#)

Linqは内部でyieldを使っていますがyieldはコンテキストスイッチを行いますので、そのパフォーマンスはどうなのか調べてみました。

yieldのパフォーマンス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Forms;

namespace Project1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

//時間計測ルーチン
public TimeSpan MeasureTime(Action action)
{
var sw = new Stopwatch();
try {
sw.Start();
action();
} finally {
sw.Stop();
}
return sw.Elapsed;
}

//整数を一千万回発生
public IEnumerable<int> GetIntegerEnumerator()
{
for (var i = 0; i < 10000000; i++) {
yield return i;
}
}

//計測対象ルーチン、yield使用
void test1()
{
foreach (var i in GetIntegerEnumerator()) {
var x = i;
}
}

//クラスで実現
class Test2 : IEnumerator<int>
{
int _current = 0;

public int Current
{
get { return _current; }
}

public void Dispose()
{

}

object System.Collections.IEnumerator.Current
{
get { return _current; }
}

public bool MoveNext()
{
_current++;
return _current<10000000;
}

public void Reset()
{
_current = 0;
}
}

//計測対象ルーチン、クラス使用
void test2()
{
foreach (var i in new Test2().ToEnumerable()) {
var x = i;
}
}

private void button1_Click(object sender, EventArgs e)
{
for (var i=0; i<5; i++) {
Console.WriteLine(MeasureTime(test1)); //yield使用
}
}

private void button2_Click(object sender, EventArgs e)
{
for (var i = 0; i < 5; i++) {
Console.WriteLine(MeasureTime(test2)); //クラス使用
}
}
}

//ユティリティ
public static class EnumeratorExtensions
{
private class EnumerableConverter<T> : IEnumerable<T>
{
IEnumerator<T> _enumerator;

public EnumerableConverter(IEnumerator<T> enumerator)
{
_enumerator = enumerator;
}

public IEnumerator<T> GetEnumerator()
{
return _enumerator;
}

IEnumerator IEnumerable.GetEnumerator()
{
return _enumerator;
}
}

//IEnumerator<T>への変換メソッド
public static IEnumerable<T> ToEnumerable<T>(this IEnumerator<T> enumerator)
{
return new EnumerableConverter<T>(enumerator);
}
}

}
リストではGetIntegerEnumeratorで整数を一千万回発生させ受け側test1ではforeachでごく軽い処理を行っています。

その結果は約0.7秒でした。

  • 00:00:00.6699127
  • 00:00:00.7322575
  • 00:00:00.6883833
  • 00:00:00.6887978
  • 00:00:00.7158395

同等の処理をクラスで実現すると、0.5秒でした。

  • 00:00:00.5362418
  • 00:00:00.5300212
  • 00:00:00.5566201
  • 00:00:00.5479106
  • 00:00:00.5479031

マシンスペックはAMD Athlon(tm) X2 Dual Core Processor BE-2350 2.11GHz、2.00 GB RAM、OSはWin

XP SP3 32Bitです。Visual Studio 2008でデバッグビルドしました。


yieldはクラスに比べると若干遅いのですが、ソースファイルやHTMLファイルのフィルタリング処理を行う程度であれば極端にパフォーマンスを落とすこともなさそうです。

バッファリングによってコンテキストスイッチの回数を減らすテストプログラムを書こうとしたのですが、複雑でパフォーマンスが上がりそうもなかったので中止しました。

その後、Reflectorで生成されたコードを分析すると、yield returnはクラスによって状態を保持するコードに置き換えられることがわかりました。私はWinAPIのファイバーの様な仕組みを想像していましたが違いました。傾向としてyield returnの記述が多ければ生成されたコードのswitchの選択肢が増えますのでパフォーマンスは悪くなります。