ダブル サンキング (C++)

ダブル サンキングとは、マネージ コンテキストでの関数呼び出しで Visual C++ マネージ関数が呼び出され、マネージ関数を呼び出すためにプログラム実行により関数のネイティブ エントリ ポイントが呼び出されるときに見られるパフォーマンスの低下を指します。 ここでは、ダブル サンキングがいつ発生するかについて説明し、これを回避してパフォーマンスを向上させるための方法を示します。

解説

既定では、(/clr:pure ではなく) /clr を指定してコンパイルすると、マネージ関数の定義によりコンパイラはマネージ エントリ ポイントとネイティブ エントリ ポイントを生成します。 これによって、マネージ関数をネイティブ呼び出しサイトとマネージ呼び出しサイトから呼び出すことができます。 しかし、ネイティブ エントリ ポイントが存在する場合は、すべての関数呼び出しのエントリ ポイントになります。 呼び出し元の関数がマネージ関数であれば、ネイティブ エントリ ポイントはその次にマネージ エントリ ポイントを呼び出します。 実質的に、関数を呼び出すために 2 つの呼び出しが必要です (ここから、ダブル サンキングと呼ばれます)。 たとえば仮想関数は常にネイティブ エントリ ポイントを通じて呼び出されます。

1 つの解決策は、__clrcall 呼び出し規約を使用して、マネージ関数に対してネイティブ エントリ ポイントを生成しないようにコンパイラに指示し、関数がマネージ コンテキストからのみ呼び出されるようにすることです。

同様に、マネージ関数をエクスポートすると (dllexport, dllimport)、ネイティブ エントリ ポイントが生成され、その関数をインポートしたり呼び出したりする任意の関数はネイティブ エントリ ポイントを通じて関数を呼び出します。 この状況でダブル サンキングを回避するには、ネイティブ エクスポート/インポート セマンティクスを使用せず、単に #using を介してメタデータを参照します。「#using Directive (C/C++)」を参照してください。

不要なダブル サンキングを減らすようにコンパイラが更新されました。 たとえば、署名にマネージ型を含む任意の関数 (戻り値の型を含みます) は、暗黙的に __clrcall とマークされます。 ダブル サンクの除去の詳細については、https://msdn.microsoft.com/msdnmag/issues/05/01/COptimizations/default.aspx を参照してください。

説明

次のサンプルはダブル サンキングを示しています。 /clr を指定せずにコンパイルした場合、main での仮想関数の呼び出しは、T のコピー コンストラクターとデストラクターへの呼び出しを 1 つずつ生成します。 同様の動作は、仮想関数を /clr および __clrcall で宣言した場合にも行われます。 一方、/clr を指定してコンパイルすると、関数呼び出しによりコピー コンストラクターへの呼び出しが生成されます。しかし、ネイティブからマネージへのサンクのために、コピー コンストラクターへの呼び出しは別にもう 1 つあります。

コード

// double_thunking.cpp
// compile with: /clr
#include <stdio.h>
struct T {
   T() {
      puts(__FUNCSIG__);
   }

   T(const T&) {
      puts(__FUNCSIG__);
   }

   ~T() {
      puts(__FUNCSIG__);
   }

   T& operator=(const T&) {
      puts(__FUNCSIG__);
      return *this;
   }
};

struct S {
   virtual void /* __clrcall */ f(T t) {};
} s;

int main() {
   S* pS = &s;
   T t;

   printf("calling struct S\n");
   pS->f(t);
   printf("after calling struct S\n");
}

出力例

__thiscall T::T(void)
calling struct S
__thiscall T::T(const struct T &)
__thiscall T::T(const struct T &)
__thiscall T::~T(void)
__thiscall T::~T(void)
after calling struct S
__thiscall T::~T(void)

説明

上のサンプルではダブル サンキングの存在を示しました。 このサンプルでは、その効果について説明します。 for ループは仮想関数を呼び出し、プログラムは実行時間を報告します。 実行時間が最も遅いのは、/clr を指定してプログラムをコンパイルした場合です。 /clr を指定せずにコンパイルした場合、または__clrcall で仮想関数を宣言した場合、実行時間は最も速くなります。

コード

// double_thunking_2.cpp
// compile with: /clr
#include <time.h>
#include <stdio.h> 

#pragma unmanaged
struct T {
   T() {}
   T(const T&) {}
   ~T() {}
   T& operator=(const T&) { return *this; }
};

struct S {
   virtual void /* __clrcall */ f(T t) {};
} s;

int main() {
   S* pS = &s;
   T t;
   clock_t start, finish;
   double  duration;
   start = clock();

   for ( int i = 0 ; i < 1000000 ; i++ )
      pS->f(t);

   finish = clock();
   duration = (double)(finish - start) / (CLOCKS_PER_SEC);
   printf( "%2.1f seconds\n", duration );
   printf("after calling struct S\n");
}

出力例

4.2 seconds
after calling struct S

参照

概念

混在 (ネイティブおよびマネージ) アセンブリ