ダブル サンキング (C++)
ダブル サンキングとは、マネージド コンテキストでの関数呼び出しにより Visual C++ マネージド関数が呼び出されるときに、そのマネージド関数を呼び出すためにプログラムの実行により関数のネイティブ エントリ ポイントが呼び出される場所で発生する、パフォーマンスの低下のことを指します。 このトピックでは、ダブル サンキングが発生する場所と、パフォーマンスを向上させるためにそれを回避する方法について説明します。
解説
既定では、/clr を指定してコンパイルすると、マネージド関数の定義によって、コンパイラでマネージド エントリ ポイントとネイティブ エントリ ポイントが生成されます。 これにより、ネイティブとマネージドの呼び出しサイトからマネージド関数を呼び出すことができます。 ただし、ネイティブ エントリ ポイントが存在する場合は、それが関数のすべての呼び出しのエントリ ポイントになることがあります。 呼び出し元の関数がマネージドの場合は、ネイティブ エントリ ポイントによってマネージド エントリ ポイントが呼び出されます。 実際には、関数を呼び出すために 2 つの呼び出しが必要になります (したがって、ダブル サンキングが発生します)。 たとえば、仮想関数は、常にネイティブ エントリ ポイントを通じて呼び出されます。
解決策の 1 つとして、マネージド関数のネイティブ エントリ ポイントを生成しないようにコンパイラに指示します。これにより、その関数は __clrcall 呼び出し規則を使用して、マネージド コンテキストからのみ呼び出されるようになります。
同様に、マネージド関数 (dllexport、dllimport) をエクスポートした場合はネイティブ エントリ ポイントが生成され、その関数をインポートして呼び出すすべての関数は、ネイティブ エントリ ポイントを通じて呼び出します。 このような状況でダブル サンキングを回避するには、ネイティブのエクスポート/インポート セマンティクスを使用しないでください。#using
を介してメタデータを参照してください (「#using ディレクティブ」を参照)。
不要なダブル サンキングを減らすために、コンパイラが更新されています。 たとえば、シグネチャ内のマネージド型を持つ関数 (戻り値の型を含む) は、暗黙的に __clrcall
としてマークされます。
例: ダブル サンキング
説明
ダブル サンキングの例を次に示します。 (/clr なしで) ネイティブでコンパイルされると、main
での仮想関数の呼び出しにより、T
のコピー コンストラクターとデストラクターへの呼び出しがそれぞれ生成されます。 同様の動作は、/clr と __clrcall
を使用して仮想関数が宣言されるときにも行われます。 ただし、/clr を使用してコンパイルしただけでは、関数呼び出しによってコピー コンストラクターへの呼び出しが生成されますが、ネイティブからマネージドへのサンクにより、コピー コンストラクターへの別の呼び出しが発生します。
コード
// 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