私のC++/ CLIプロジェクトでは、C ++/CLI関数ポインターと.NETデリゲートのコストを測定しようとしました。
私の期待は、C ++/CLI関数ポインターが.NETデリゲートよりも高速であるということでした。したがって、私のテストでは、5秒間の.NETデリゲートとネイティブ関数ポインターの呼び出し数を個別にカウントします。
結果
今、結果は私に衝撃的でした(そして今でもそうです):
- .NETデリゲート: 900Mの実行、5003ミリ秒で結果152080413333030
- 関数ポインタ: 5013ミリ秒で結果57893422166551の347Mの実行
つまり、ネイティブC ++ / CLI関数ポインターの使用は、C ++/CLIコード内からマネージデリゲートを使用する場合よりもほぼ3倍遅くなります。どうしてそれができるのでしょうか?パフォーマンスクリティカルセクションでインターフェイス、デリゲート、または抽象クラスを使用する場合は、マネージ構造を使用する必要がありますか?
テストコード
継続的に呼び出される関数:
__int64 DoIt(int n, __int64 sum)
{
if ((n % 3) == 0)
return sum + n;
else
return sum + 1;
}
メソッドを呼び出すコードは、戻り値だけでなくすべてのパラメーターを利用しようとするため、最適化されるものはありません(うまくいけば)。コードは次のとおりです(.NETデリゲート用):
__int64 executions;
__int64 result;
System::Diagnostics::Stopwatch^ w = gcnew System::Diagnostics::Stopwatch();
System::Func<int, __int64, __int64>^ managedPtr = gcnew System::Func<int, __int64, __int64>(&DoIt);
w->Restart();
executions = 0;
result = 0;
while (w->ElapsedMilliseconds < 5000)
{
for (int i=0; i < 1000000; i++)
result += managedPtr(i, executions);
executions++;
}
System::Console::WriteLine(".NET delegate: {0}M executions with result {2} in {1}ms", executions, w->ElapsedMilliseconds, result);
.NETデリゲート呼び出しと同様に、C++関数ポインターが使用されます。
typedef __int64 (* DoItMethod)(int n, __int64 sum);
DoItMethod nativePtr = DoIt;
w->Restart();
executions = 0;
result = 0;
while (w->ElapsedMilliseconds < 5000)
{
for (int i=0; i < 1000000; i++)
result += nativePtr(i, executions);
executions++;
}
System::Console::WriteLine("Function pointer: {0}M executions with result {2} in {1}ms", executions, w->ElapsedMilliseconds, result);
追加情報
- VisualStudio2012でコンパイル
- .NETFramework4.5がターゲットになりました
- リリースビルド(実行カウントはデバッグビルドに対して比例したままです)
- 呼び出し規約は__stdcallです(プロジェクトがCLRサポートでコンパイルされる場合は__fastcallは許可されません)
行われたすべてのテスト:
- .NET仮想メソッド:5004msで結果171358304166325の1025M実行
- .NETデリゲート:900Mの実行、5003ミリ秒で結果152080413333030
- 仮想メソッド:5006msで結果56056335999888の336M実行
- 関数ポインタ:5013ミリ秒で結果57893422166551の347Mの実行
- 関数呼び出し:5001msで結果244230520832847を伴う1459Mの実行
- インライン関数:1385Mの実行、5000ミリ秒で231791984166205の結果
「DoIt」への直接呼び出しは、ここでは「関数呼び出し」で表されます。これは、インライン化された関数の呼び出しと比較して実行回数に(有意な)違いがないため、コンパイラーによってインライン化されているように見えます。
C ++仮想メソッドの呼び出しは、関数ポインターと同じくらい「遅い」です。管理対象クラス(refクラス)の仮想メソッドは、.NETデリゲートと同じくらい高速です。
更新: もう少し深く掘り下げました。アンマネージ関数を使用したテストでは、DoIt関数が呼び出されるたびにネイティブコードへの移行が行われるようです。したがって、内部ループを別の関数にラップし、アンマネージでコンパイルするように強制しました。
#pragma managed(push, off)
__int64 TestCall(__int64* executions)
{
__int64 result = 0;
for (int i=0; i < 1000000; i++)
result += DoItNative(i, *executions);
(*executions)++;
return result;
}
#pragma managed(pop)
さらに、私は次のようなstd::functionをテストしました。
#pragma managed(push, off)
__int64 TestStdFunc(__int64* executions)
{
__int64 result = 0;
std::function<__int64(int, __int64)> func(DoItNative);
for (int i=0; i < 1000000; i++)
result += func(i, *executions);
(*executions)++;
return result;
}
#pragma managed(pop)
現在、新しい結果は次のとおりです。
- 関数呼び出し:5000msで結果49534043997054の2946M実行
- std :: function:1億6000万回の実行、5018ミリ秒で26679519999840の結果
std::functionは少し残念です。