継承とジェネリックを使用するときにコード全体で使用するパフォーマンス特性を理解するのに苦労していFunc<...>
ます。これは、私が常に使用している組み合わせです。
最小限のテストケースから始めて、私たちが話していることを理解してから、結果を投稿してから、期待されることとその理由を説明します...
最小限のテストケース
public class GenericsTest2 : GenericsTest<int>
{
static void Main(string[] args)
{
GenericsTest2 at = new GenericsTest2();
at.test(at.func);
at.test(at.Check);
at.test(at.func2);
at.test(at.Check2);
at.test((a) => a.Equals(default(int)));
Console.ReadLine();
}
public GenericsTest2()
{
func = func2 = (a) => Check(a);
}
protected Func<int, bool> func2;
public bool Check2(int value)
{
return value.Equals(default(int));
}
public void test(Func<int, bool> func)
{
using (Stopwatch sw = new Stopwatch((ts) => { Console.WriteLine("Took {0:0.00}s", ts.TotalSeconds); }))
{
for (int i = 0; i < 100000000; ++i)
{
func(i);
}
}
}
}
public class GenericsTest<T>
{
public bool Check(T value)
{
return value.Equals(default(T));
}
protected Func<T, bool> func;
}
public class Stopwatch : IDisposable
{
public Stopwatch(Action<TimeSpan> act)
{
this.act = act;
this.start = DateTime.UtcNow;
}
private Action<TimeSpan> act;
private DateTime start;
public void Dispose()
{
act(DateTime.UtcNow.Subtract(start));
}
}
結果
Took 2.50s -> at.test(at.func);
Took 1.97s -> at.test(at.Check);
Took 2.48s -> at.test(at.func2);
Took 0.72s -> at.test(at.Check2);
Took 0.81s -> at.test((a) => a.Equals(default(int)));
私が期待することとその理由
私は、このコードが 5 つのメソッドすべてでまったく同じ速度で実行されることを期待していました。より正確には、これよりもさらに速く、つまり次のように高速です。
using (Stopwatch sw = new Stopwatch((ts) => { Console.WriteLine("Took {0:0.00}s", ts.TotalSeconds); }))
{
for (int i = 0; i < 100000000; ++i)
{
bool b = i.Equals(default(int));
}
}
// this takes 0.32s ?!?
この特定のケースで JIT コンパイラーがコードをインライン化しない理由が見当たらないので、0.32 秒かかると予想しました。
よく調べてみると、これらのパフォーマンスの数値がまったくわかりません。
at.func
関数に渡され、実行中に変更することはできません。これがインライン化されていないのはなぜですか?at.Check
は明らかに より高速ですがat.Check2
、どちらもオーバーライドできず、クラス GenericsTest2 の場合の at.Check の IL は岩のように固定されています。- に変換されたメソッドの代わりに
Func<int, bool>
インラインを渡すときに遅くなる理由はわかりませんFunc
Func
- そして、なぜテスト ケース 2 と 3 の差はなんと 0.5 秒なのに対し、ケース 4 と 5 の差は 0.1 秒なのでしょうか。
質問
私は本当にこれを理解したいと思います...ジェネリック基本クラスを使用すると、全体をインライン化するよりもなんと10倍も遅いということは、ここで何が起こっているのでしょうか?
したがって、基本的な質問は、なぜこれが起こっているのか、どうすれば修正できるのかということです。
アップデート
これまでのすべてのコメントに基づいて (ありがとう!)、さらに掘り下げました。
まず、テストを繰り返し、ループを 5 倍大きくして 4 回実行したときの新しい結果セット。診断ストップウォッチを使用し、さらにテストを追加しました (説明も追加しました)。
(Baseline implementation took 2.61s)
--- Run 0 ---
Took 3.00s for (a) => at.Check2(a)
Took 12.04s for Check3<int>
Took 12.51s for (a) => GenericsTest2.Check(a)
Took 13.74s for at.func
Took 16.07s for GenericsTest2.Check
Took 12.99s for at.func2
Took 1.47s for at.Check2
Took 2.31s for (a) => a.Equals(default(int))
--- Run 1 ---
Took 3.18s for (a) => at.Check2(a)
Took 13.29s for Check3<int>
Took 14.10s for (a) => GenericsTest2.Check(a)
Took 13.54s for at.func
Took 13.48s for GenericsTest2.Check
Took 13.89s for at.func2
Took 1.94s for at.Check2
Took 2.61s for (a) => a.Equals(default(int))
--- Run 2 ---
Took 3.18s for (a) => at.Check2(a)
Took 12.91s for Check3<int>
Took 15.20s for (a) => GenericsTest2.Check(a)
Took 12.90s for at.func
Took 13.79s for GenericsTest2.Check
Took 14.52s for at.func2
Took 2.02s for at.Check2
Took 2.67s for (a) => a.Equals(default(int))
--- Run 3 ---
Took 3.17s for (a) => at.Check2(a)
Took 12.69s for Check3<int>
Took 13.58s for (a) => GenericsTest2.Check(a)
Took 14.27s for at.func
Took 12.82s for GenericsTest2.Check
Took 14.03s for at.func2
Took 1.32s for at.Check2
Took 1.70s for (a) => a.Equals(default(int))
これらの結果から、ジェネリックを使用し始めた瞬間に、速度が大幅に低下することに気付きました。非ジェネリック実装で見つけた IL をもう少し掘り下げます。
L_0000: ldarga.s 'value'
L_0002: ldc.i4.0
L_0003: call instance bool [mscorlib]System.Int32::Equals(int32)
L_0008: ret
そして、すべての一般的な実装について:
L_0000: ldarga.s 'value'
L_0002: ldloca.s CS$0$0000
L_0004: initobj !T
L_000a: ldloc.0
L_000b: box !T
L_0010: constrained. !T
L_0016: callvirt instance bool [mscorlib]System.Object::Equals(object)
L_001b: ret
これのほとんどは最適化callvirt
できますが、ここで問題になる可能性があると思います。
高速化するために、メソッドの定義に 'T : IEquatable' 制約を追加しました。結果は次のとおりです。
L_0011: callvirt instance bool [mscorlib]System.IEquatable`1<!T>::Equals(!0)
パフォーマンスについては理解できましたが (vtable ルックアップを作成するため、おそらくインライン化できません)、まだ混乱しています: 単純に T::Equals を呼び出さないのはなぜですか? 結局のところ、私はそれがそこにあると明記しています...