ダブル サンキング (C++)
ダブル サンキングとは、マネージ コンテキストでの関数呼び出しで Visual C++ マネージ関数が呼び出され、マネージ関数を呼び出すためにプログラム実行により関数のネイティブ エントリ ポイントが呼び出されるときに見られるパフォーマンスの低下を指します。ここでは、ダブル サンキングがいつ発生するかについて説明し、これを回避してパフォーマンスを向上させるための方法を示します。
解説
既定では、(/clr:pure ではなく) /clr を指定してコンパイルすると、マネージ関数の定義によりコンパイラはマネージ エントリ ポイントとネイティブ エントリ ポイントを生成します。これによって、マネージ関数をネイティブ呼び出しサイトとマネージ呼び出しサイトから呼び出すことができます。しかし、ネイティブ エントリ ポイントが存在する場合は、すべての関数呼び出しのエントリ ポイントになります。呼び出し元の関数がマネージ関数であれば、ネイティブ エントリ ポイントはその次にマネージ エントリ ポイントを呼び出します。実質的に、関数を呼び出すために 2 つの呼び出しが必要です (ここから、ダブル サンキングと呼ばれます)。たとえば仮想関数は常にネイティブ エントリ ポイントを通じて呼び出されます。
1 つの解決策は、__clrcall 呼び出し規約を使用して、マネージ関数に対してネイティブ エントリ ポイントを生成しないようにコンパイラに指示し、関数がマネージ コンテキストからのみ呼び出されるようにすることです。
同様に、マネージ関数をエクスポートすると (dllexport、 dllimport)、ネイティブ エントリ ポイントが生成され、その関数をインポートしたり呼び出したりする任意の関数はネイティブ エントリ ポイントを通じて関数を呼び出します。この状況でダブル サンキングを回避するには、ネイティブ エクスポート/インポート セマンティクスを使用せず、単に #using を介してメタデータを参照します。「#using ディレクティブ (C++)」を参照してください。
不要なダブル サンキングを減らすようにコンパイラが更新されました。たとえば、署名にマネージ型を含む任意の関数 (戻り値の型を含みます) は、暗黙的に __clrcall とマークされます。ダブル サンクの除去の詳細については、https://msdn.microsoft.com/msdnmag/issues/05/01/COptimizations/default.aspx を参照してください。
例
Description
次のサンプルはダブル サンキングを示しています。/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)
例
Description
上のサンプルではダブル サンキングの存在を示しました。このサンプルでは、その効果について説明します。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