이중 썽킹(C++)
이중 썽킹은 관리되는 컨텍스트의 함수 호출로 Visual C++ 관리되는 함수를 호출할 때 관리되는 함수를 호출하기 위해 함수의 네이티브 진입점을 호출하는 프로그램 실행에서 발생할 수 있는 성능 저하를 가리킵니다. 이 항목에서는 이중 썽킹이 발생하는 위치와 이를 방지하여 성능을 향상시키는 방법에 대해 설명합니다.
설명
기본적으로 /clr:pure가 아닌 /clr를 사용하여 컴파일하는 경우 관리되는 함수의 정의로 인해 컴파일러에서 관리되는 진입점과 네이티브 진입점이 생성됩니다. 이 경우 네이티브 및 관리되는 호출 사이트에서 관리되는 함수를 호출할 수 있습니다. 그러나 네이티브 진입점이 있는 경우 이는 함수에 대한 모든 호출의 진입점으로 사용될 수 있습니다. 호출 함수가 관리되는 경우 네이티브 진입점은 관리되는 진입점을 호출합니다. 실제로 함수를 호출하려면 두 호출이 모두 필요하므로 이중 썽킹이 발생합니다. 예를 들어, 가상 함수는 항상 네이티브 진입점을 통해 호출됩니다.
이를 해결하는 한 가지 방법은 __clrcall 호출 규칙을 사용하여 함수가 관리되는 컨텍스트에서만 호출되고 관리되는 함수의 네이티브 진입점을 생성하지 않도록 컴파일러에 지시하는 것입니다.
마찬가지로, 관리되는 함수를 내보내는 경우(dllexport, dllimport) 네이티브 진입점이 생성되고 해당 함수를 가져오고 호출하는 모든 함수가 네이티브 진입점을 통해 호출합니다. 이 상황에서 이중 썽킹이 발생하지 않도록 하려면 네이티브 내보내기/가져오기 의미를 사용하지 말고 #using을 통해 메타데이터를 참조해야 합니다. 자세한 내용은 #using Directive (C/C++)를 참조하십시오.
불필요한 이중 썽킹을 줄이기 위해 컴파일러가 업데이트되었습니다. 예를 들어, 반환 형식을 비롯하여 시그니처에 관리되는 형식이 있는 함수는 모두 암시적으로 __clrcall로 표시됩니다. 이중 썽킹 제거에 대한 자세한 내용은 https://msdn.microsoft.com/msdnmag/issues/05/01/COptimizations/default.aspx를 참조하십시오.
예제
설명
다음 샘플에서는 이중 썽킹을 보여 줍니다. /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