3

Visual Studio 2010を使用して、ネイティブc++からメソッドを呼び出し、 c++/cliからc#を呼び出すパフォーマンス (ミリ秒単位の時間) をテストするために、以下のコードをいくつか投稿しました。dll にコンパイルされる別のネイティブ C++ プロジェクトがあります。C++ から C++ を呼び出すと、期待される結果が得られます。これは、対応するマネージ コードよりもはるかに高速 (約 4 倍) です。ただし、c++/cli から c++ を呼び出すと、パフォーマンスが 10 倍遅くなります。

これは、c++/cli からネイティブ C++ を呼び出すときに想定される動作ですか? 大きな違いはないはずだという印象を受けましたが、この単純なテストではそうではありません。これは、c++ コンパイラと c++/cli コンパイラの最適化の違いでしょうか?

アップデート

私は cpp にいくつかの更新を行ったので、(Reed Copsey が指摘したように) タイトなループでメソッドを呼び出さないようにしましたが、パフォーマンスの違いは取るに足らないか非常に小さいことがわかりました。もちろん、相互運用がどのように行われているかによって異なります。

.h

#ifndef CPPOBJECT_H
#define CPPOBJECT_H

#ifdef CPLUSPLUSOBJECT_EXPORTING
    #define CLASS_DECLSPEC __declspec(dllexport)
#else
    #define CLASS_DECLSPEC __declspec(dllimport)
#endif

class CLASS_DECLSPEC CPlusPlusObject
{
public:
    CPlusPlusObject(){}
    ~CPlusPlusObject(){}

    void sayHello();
    double getSqrt(double n);
    // Update
    double wasteSomeTimeWithSqrt(double n);
};

#endif

.cpp

#include "CPlusPlusObject.h"
#include <iostream>

void CPlusPlusObject::sayHello(){std::cout << "Hello";}
double CPlusPlusObject::getSqrt(double n) {return std::sqrt(n);}
double CPlusPlusObject::wasteSomeTimeWithSqrt(double n)
{
    double result = 0;
    for (int x = 0; x < 10000000; x++)
    {
        result += std::sqrt(n);
    }
    return result;
}

c++/クリ

const unsigned set = 100;
const unsigned repetitions = 1000000;
double cppcliTocpp()
{
    double n = 0;
    System::Diagnostics::Stopwatch^ stopWatch = gcnew System::Diagnostics::Stopwatch();

     stopWatch->Start();
     while (stopWatch->ElapsedMilliseconds < 1200){n+=0.001;}
     stopWatch->Reset();

    for (int x = 0; x < set; x++)
    {       
        stopWatch->Start();
        CPlusPlusObject cplusplusObject;
        n += cplusplusObject.wasteSomeTimeWithSqrt(123.456);
        /*for (int i = 0; i < repetitions; i++)
        {
            n += cplusplusObject.getSqrt(123.456);
        }*/
        stopWatch->Stop();
        System::Console::WriteLine("c++/cli call to native c++ took " + stopWatch->ElapsedMilliseconds + "ms.");
        stopWatch->Reset();
    }
    return n;
}

double cppcliTocSharp()
{
    double n = 0;
    System::Diagnostics::Stopwatch^ stopWatch = gcnew System::Diagnostics::Stopwatch();

    stopWatch->Start();
    while (stopWatch->ElapsedMilliseconds < 1200){n+=0.001;}
    stopWatch->Reset();

    for (int x = 0; x < set; x++)
    {       
        stopWatch->Start();
        CSharp::CSharpObject^ cSharpObject = gcnew CSharp::CSharpObject();
        for (int i = 0; i < repetitions; i++)
        {
            n += cSharpObject->GetSqrt(123.456);
        }
        stopWatch->Stop();
        System::Console::WriteLine("c++/cli call to c# took " + stopWatch->ElapsedMilliseconds + "ms.");
        stopWatch->Reset();
    }
    return n;
}

double cppcli()
{
    double n = 0;
    System::Diagnostics::Stopwatch^ stopWatch = gcnew System::Diagnostics::Stopwatch();

    stopWatch->Start();
    while (stopWatch->ElapsedMilliseconds < 1200){n+=0.001;}
    stopWatch->Reset();

    for (int x = 0; x < set; x++)
    {       
        stopWatch->Start();
        CPlusPlusCliObject cPlusPlusCliObject;
        for (int i = 0; i < repetitions; i++)
        {
            n += cPlusPlusCliObject.getSqrt(123.456);
        }
        stopWatch->Stop();
        System::Console::WriteLine("c++/cli took " + stopWatch->ElapsedMilliseconds + "ms.");
        stopWatch->Reset();
    }
    return n;
}

int main() 
{
    double n = 0;
    n += cppcliTocpp();
    n += cppcliTocSharp();
    n += cppcli();
    System::Console::WriteLine(n);
    System::Console::ReadKey();
}
4

2 に答える 2

4

ただし、c++/cli から c++ を呼び出すと、パフォーマンスが 10 倍遅くなります。

CLR とネイティブ コードを橋渡しするには、マーシャリングが必要です。C++/CLI からネイティブ メソッド呼び出しに移行する場合、各メソッド呼び出しには常にいくらかのオーバーヘッドが発生します。

オーバーヘッド (この場合) が非常に大きく見える唯一の理由は、タイトなループで非常に高速なメソッドを呼び出しているためです。クラスをバッチ処理したり、実行時間が大幅に長いメソッドを呼び出したりする場合、オーバーヘッドは非常に小さいことがわかります。

于 2013-09-23T18:41:09.710 に答える
1

これらのマイクロベンチマークは非常に危険です。典型的なベンチマークの間違いを回避しようと努力しましたが、それでも古典的な罠に陥りました。あなたの意図はメソッド呼び出しのオーバーヘッドを測定することでしたが、それは実際に起こっていることではありません。ジッター オプティマイザーは、コードの巻き上げやメソッドのインライン化など、標準的なコード最適化手法を実行できます。これは、生成されたマシン コードを見たときにのみ確認できます。デバッグ + ウィンドウ + 逆アセンブリ ウィンドウ。

これは、ジッター オプティマイザーを有効にした VS2012 の 32 ビット リリース ビルドでテストしました。C++/CLI コードが最速で、約 128 ミリ秒かかりました。

000000bf  fld         qword ptr ds:[01212078h] 
000000c5  fsqrt 
000000c7  fstp        qword ptr [ebp-20h] 
//
// stopWatch->Start() call elided...
//
            n += cPlusPlusCliObject.getSqrt(123.456);
000000f5  fld         qword ptr [ebp-20h] 
000000f8  fadd        qword ptr [ebp-14h] 
000000fb  fstp        qword ptr [ebp-14h] 
        for (int i = 0; i < repetitions; i++)
000000fe  dec         eax 
000000ff  jne         000000F5 

つまり、std::sqrt() 呼び出しはループから引き上げられ、内側のループは生成された値から加算を実行するだけです。メソッド呼び出しなし。また、 sqrt() 呼び出しに必要な時間を実際に測定しなかったことに注意してください:)

C# メソッド呼び出しのループは少し遅く、約 180 ミリ秒かかりました。

000000ea  fld         qword ptr ds:[01211EC0h] 
000000f0  fsqrt 
000000f2  fadd        qword ptr [ebp-14h] 
000000f5  fstp        qword ptr [ebp-14h] 
        for (int i = 0; i < repetitions; i++)
000000f8  dec         eax 
000000f9  jne         000000EA 

Math::Sqrt() へのインライン メソッド呼び出しだけで、巻き上げられませんでした。実際の理由はわかりませんが、ジッター オプティマイザによって実行される最適化には時間要素が含まれています。

また、相互運用呼び出しのコードは投稿しません。しかし、はい、実際に関数呼び出しを行う必要があるため、最大 380 ミリ秒かかるため、アンマネージ コードをインライン化することはできません。さらに、ガベージ コレクターがアンマネージ スタック フレームに取り込まれないようにするために必要なサンクも必要です。サンクは非常に高速で、数ナノ秒かかりますが、fadd または fsqrt を直接インライン化するジッター オプティマイザーと競合することはできません。

于 2013-09-23T19:34:02.680 に答える