11

私の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は少し残念です。

4

1 に答える 1

17

あなたは「ダブルサンキング」のコストを見ています。DoIt()関数の主な問題は、マネージコードとしてコンパイルされていることです。デリゲート呼び出しは非常に高速で、デリゲートを介してマネージコードからマネージコードに移行するのは簡単です。関数ポインターは低速ですが、コンパイラーは自動的にコードを生成して、最初にマネージコードからアンマネージコードに切り替え、関数ポインターを介して呼び出しを行います。その後、マネージコードからマネージコードに戻り、DoIt()を呼び出すスタブになります。

おそらく、実際に測定するつもりだったのは、ネイティブコードの呼び出しでした。#pragmaを使用して、次のようにDoIt()をマシンコードとして強制的に生成します。

#pragma managed(push, off)
__int64 DoIt(int n, __int64 sum)
{
    if ((n % 3) == 0)
        return sum + n;
    else
        return sum + 1;
}
#pragma managed(pop)

これで、関数ポインタがデリゲートよりも高速であることがわかります。

于 2012-11-18T19:19:00.660 に答える