1-2 nanoseconds
を完了するのに非常に最適化された数学関数がいくつかあります。これらの関数は1秒間に数億回呼び出されるため、既に優れたパフォーマンスを発揮しているにもかかわらず、呼び出しのオーバーヘッドが懸念事項です。
プログラムを保守可能な状態に保つために、これらのメソッドを提供するクラスはIMathFunction
インターフェースを継承するため、他のオブジェクトは特定の数学関数を直接保存し、必要なときに使用できます。
public interface IMathFunction
{
double Calculate(double input);
double Derivate(double input);
}
public SomeObject
{
// Note: There are cases where this is mutable
private readonly IMathFunction mathFunction_;
public double SomeWork(double input, double step)
{
var f = mathFunction_.Calculate(input);
var dv = mathFunction_.Derivate(input);
return f - (dv * step);
}
}
このインターフェイスは、消費するコードがそれを使用する方法のために、直接呼び出しと比較して莫大なオーバーヘッドを引き起こしています。 直接呼び出しには1-2nsかかります、仮想インターフェース呼び出しには8-9nsかかります。明らかに、インターフェースの存在とそれに続く仮想コールの変換が、このシナリオのボトルネックです。
可能であれば、保守性とパフォーマンスの両方を維持したいと思います。 オブジェクトがインスタンス化されたときに仮想関数を直接呼び出しに解決して、後続のすべての呼び出しでオーバーヘッドを回避できる方法はありますか?これにはILでデリゲートを作成することが含まれると思いますが、どこから始めればいいのか分からないでしょう。
そのため、これには明らかな制限があり、インターフェイスを持っている場所では常に使用すべきではありませんが、perfを本当に最大化する必要がある場所がある場合は、ジェネリックを使用できます。
public SomeObject<TMathFunction> where TMathFunction: struct, IMathFunction
{
private readonly TMathFunction mathFunction_;
public double SomeWork(double input, double step)
{
var f = mathFunction_.Calculate(input);
var dv = mathFunction_.Derivate(input);
return f - (dv * step);
}
}
そして、インターフェイスを渡す代わりに、実装をTMathFunctionとして渡します。これにより、インターフェースによるvtableルックアップが回避され、インライン化も可能になります。
ここでは、struct
の使用が重要であることに注意してください。そうでない場合、ジェネリックはインターフェイスを介してクラスにアクセスします。
一部の実装:
テスト用にIMathFunctionの簡単な実装を作成しました。
class SomeImplementationByRef : IMathFunction
{
public double Calculate(double input)
{
return input + input;
}
public double Derivate(double input)
{
return input * input;
}
}
...構造体バージョンと抽象バージョン。
それで、ここではインターフェイスバージョンで何が起こるかを示します。 2レベルの間接化を実行するため、比較的非効率的であることがわかります。
return obj.SomeWork(input, step);
sub esp,40h
vzeroupper
vmovaps xmmword ptr [rsp+30h],xmm6
vmovaps xmmword ptr [rsp+20h],xmm7
mov rsi,rcx
vmovsd qword ptr [rsp+60h],xmm2
vmovaps xmm6,xmm1
mov rcx,qword ptr [rsi+8] ; load mathFunction_ into rcx.
vmovaps xmm1,xmm6
mov r11,7FFED7980020h ; load vtable address of the IMathFunction.Calculate function.
cmp dword ptr [rcx],ecx
call qword ptr [r11] ; call IMathFunction.Calculate function which will call the actual Calculate via vtable.
vmovaps xmm7,xmm0
mov rcx,qword ptr [rsi+8] ; load mathFunction_ into rcx.
vmovaps xmm1,xmm6
mov r11,7FFED7980028h ; load vtable address of the IMathFunction.Derivate function.
cmp dword ptr [rcx],ecx
call qword ptr [r11] ; call IMathFunction.Derivate function which will call the actual Derivate via vtable.
vmulsd xmm0,xmm0,mmword ptr [rsp+60h] ; dv * step
vsubsd xmm7,xmm7,xmm0 ; f - (dv * step)
vmovaps xmm0,xmm7
vmovaps xmm6,xmmword ptr [rsp+30h]
vmovaps xmm7,xmmword ptr [rsp+20h]
add rsp,40h
pop rsi
ret
これが抽象クラスです。それはもう少し効率的ですが、ごくわずかです:
return obj.SomeWork(input, step);
sub esp,40h
vzeroupper
vmovaps xmmword ptr [rsp+30h],xmm6
vmovaps xmmword ptr [rsp+20h],xmm7
mov rsi,rcx
vmovsd qword ptr [rsp+60h],xmm2
vmovaps xmm6,xmm1
mov rcx,qword ptr [rsi+8] ; load mathFunction_ into rcx.
vmovaps xmm1,xmm6
mov rax,qword ptr [rcx] ; load object type data from mathFunction_.
mov rax,qword ptr [rax+40h] ; load address of vtable into rax.
call qword ptr [rax+20h] ; call Calculate via offset 0x20 of vtable.
vmovaps xmm7,xmm0
mov rcx,qword ptr [rsi+8] ; load mathFunction_ into rcx.
vmovaps xmm1,xmm6
mov rax,qword ptr [rcx] ; load object type data from mathFunction_.
mov rax,qword ptr [rax+40h] ; load address of vtable into rax.
call qword ptr [rax+28h] ; call Derivate via offset 0x28 of vtable.
vmulsd xmm0,xmm0,mmword ptr [rsp+60h] ; dv * step
vsubsd xmm7,xmm7,xmm0 ; f - (dv * step)
vmovaps xmm0,xmm7
vmovaps xmm6,xmmword ptr [rsp+30h]
vmovaps xmm7,xmmword ptr [rsp+20h]
add rsp,40h
pop rsi
ret
したがって、インターフェースと抽象クラスの両方は、許容可能なパフォーマンスを得るために分岐ターゲット予測に大きく依存しています。それでも、かなり多くのことが行われていることがわかります。そのため、ベストケースは依然として比較的低速ですが、ワーストケースは予測ミスによるパイプラインの停止です。
最後に、構造体を含む汎用バージョンを示します。すべてが完全にインライン化されているため、分岐予測が含まれていないため、非常に効率的であることがわかります。また、そこにあったスタック/パラメータ管理のほとんどを削除するという素晴らしい副作用があるため、コードは非常にコンパクトになります。
return obj.SomeWork(input, step);
Push rax
vzeroupper
movsx rax,byte ptr [rcx+8]
vmovaps xmm0,xmm1
vaddsd xmm0,xmm0,xmm1 ; Calculate - got inlined
vmulsd xmm1,xmm1,xmm1 ; Derivate - got inlined
vmulsd xmm1,xmm1,xmm2 ; dv * step
vsubsd xmm0,xmm0,xmm1 ; f -
add rsp,8
ret
メソッドをデリゲートに割り当てます。これにより、インターフェイスメソッドの解決を避けながら、インターフェイスに対してプログラミングを行うことができます。
_public SomeObject
{
private readonly Func<double, double> _calculate;
private readonly Func<double, double> _derivate;
public SomeObject(IMathFunction mathFunction)
{
_calculate = mathFunction.Calculate;
_derivate = mathFunction.Derivate;
}
public double SomeWork(double input, double step)
{
var f = _calculate(input);
var dv = _derivate(input);
return f - (dv * step);
}
}
_
@CoryNelsonのコメントに応えて、テストを行ったので、実際の影響を確認してください。私は関数クラスを封印しましたが、私のメソッドは仮想ではないので、これはまったく違いがないようです。
空のメソッド時間を中括弧で引いたテスト結果(nsでの1億回の反復の平均時間):
空の作業方法:1.48
インターフェース:5.69(4.21)
デリゲート:5.78(4.30)
密閉クラス:2.10(0.62)
クラス:2.12(0.64)
デリゲートバージョンの時間は、インターフェイスバージョンの時間とほぼ同じです(正確な時間はテストの実行ごとに異なります)。クラスに対して作業している間は、約6.8倍高速です(時間と空の作業メソッド時間の差)。これは、デリゲートと連携するという私の提案が役に立たなかったことを意味します!
驚いたのは、インターフェイスバージョンの実行時間がはるかに長くなることを期待していたことです。この種のテストはOPのコードの正確なコンテキストを表さないため、その有効性は制限されています。
_static class TimingInterfaceVsDelegateCalls
{
const int N = 100_000_000;
const double msToNs = 1e6 / N;
static SquareFunctionSealed _mathFunctionClassSealed;
static SquareFunction _mathFunctionClass;
static IMathFunction _mathFunctionInterface;
static Func<double, double> _calculate;
static Func<double, double> _derivate;
static TimingInterfaceVsDelegateCalls()
{
_mathFunctionClass = new SquareFunction();
_mathFunctionClassSealed = new SquareFunctionSealed();
_mathFunctionInterface = _mathFunctionClassSealed;
_calculate = _mathFunctionInterface.Calculate;
_derivate = _mathFunctionInterface.Derivate;
}
interface IMathFunction
{
double Calculate(double input);
double Derivate(double input);
}
sealed class SquareFunctionSealed : IMathFunction
{
public double Calculate(double input)
{
return input * input;
}
public double Derivate(double input)
{
return 2 * input;
}
}
class SquareFunction : IMathFunction
{
public double Calculate(double input)
{
return input * input;
}
public double Derivate(double input)
{
return 2 * input;
}
}
public static void Test()
{
var stopWatch = new Stopwatch();
stopWatch.Start();
for (int i = 0; i < N; i++) {
double result = SomeWorkEmpty(i);
}
stopWatch.Stop();
double emptyTime = stopWatch.ElapsedMilliseconds * msToNs;
Console.WriteLine($"Empty Work method: {emptyTime:n2}");
stopWatch.Restart();
for (int i = 0; i < N; i++) {
double result = SomeWorkInterface(i);
}
stopWatch.Stop();
PrintResult("Interface", stopWatch.ElapsedMilliseconds, emptyTime);
stopWatch.Restart();
for (int i = 0; i < N; i++) {
double result = SomeWorkDelegate(i);
}
stopWatch.Stop();
PrintResult("Delegates", stopWatch.ElapsedMilliseconds, emptyTime);
stopWatch.Restart();
for (int i = 0; i < N; i++) {
double result = SomeWorkClassSealed(i);
}
stopWatch.Stop();
PrintResult("Sealed Class", stopWatch.ElapsedMilliseconds, emptyTime);
stopWatch.Restart();
for (int i = 0; i < N; i++) {
double result = SomeWorkClass(i);
}
stopWatch.Stop();
PrintResult("Class", stopWatch.ElapsedMilliseconds, emptyTime);
}
private static void PrintResult(string text, long elapsed, double emptyTime)
{
Console.WriteLine($"{text}: {elapsed * msToNs:n2} ({elapsed * msToNs - emptyTime:n2})");
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static double SomeWorkEmpty(int i)
{
return 0.0;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static double SomeWorkInterface(int i)
{
double f = _mathFunctionInterface.Calculate(i);
double dv = _mathFunctionInterface.Derivate(i);
return f - (dv * 12.34534);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static double SomeWorkDelegate(int i)
{
double f = _calculate(i);
double dv = _derivate(i);
return f - (dv * 12.34534);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static double SomeWorkClassSealed(int i)
{
double f = _mathFunctionClassSealed.Calculate(i);
double dv = _mathFunctionClassSealed.Derivate(i);
return f - (dv * 12.34534);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static double SomeWorkClass(int i)
{
double f = _mathFunctionClass.Calculate(i);
double dv = _mathFunctionClass.Derivate(i);
return f - (dv * 12.34534);
}
}
_
[MethodImpl(MethodImplOptions.NoInlining)]
の考え方は、メソッドがインライン化された場合、コンパイラーがループの前にメソッドのアドレスを計算しないようにすることです。