Conversão dupla (C++)
Conversão dupla refere-se à perda de desempenho que você pode experimentar quando uma chamada de função em um contexto gerenciado chama uma função gerenciada do Visual C++ e quando a execução do programa chama o ponto de entrada nativo da função para chamar a função gerenciada. Este tópico discute quando ocorre uma conversão dupla e como você pode evitá-la para aprimorar o desempenho.
Comentários
Por padrão, ao compilar com /clr, a definição de uma função gerenciada faz com que o compilador gere um ponto de entrada gerenciado e um ponto de entrada nativo. Isso permite que a função gerenciada seja chamada de sites de chamadas nativos e gerenciados. No entanto, quando um ponto de entrada nativo existe, ele pode ser o ponto de entrada para todas as chamadas para a função. Se uma função de chamada for gerenciada, o ponto de entrada nativo chamará o ponto de entrada gerenciado. Na verdade, duas chamadas são necessárias para invocar a função (portanto, conversão dupla). Por exemplo, as funções virtuais são sempre chamadas por meio de um ponto de entrada nativo.
Uma resolução é informar ao compilador para não gerar um ponto de entrada nativo para uma função gerenciada, que a função só será chamada de um contexto gerenciado usando a convenção de chamada __clrcall.
Da mesma forma, se você exportar (dllexport, dllimport) uma função gerenciada, um ponto de entrada nativo será gerado e qualquer função que importe e chame essa função chamará por meio do ponto de entrada nativo. Para evitar a conversão dupla nessa situação, não use a semântica de exportação/importação nativa; basta fazer referência aos metadados por meio de #using
(confira Diretiva #using).
O compilador foi atualizado para reduzir a conversão dupla desnecessária. Por exemplo, qualquer função com um tipo gerenciado na assinatura (incluindo o tipo de retorno) será marcada implicitamente como __clrcall
.
Exemplo: conversão dupla
Descrição
O exemplo a seguir demonstra a conversão dupla. Quando compilada nativamente (sem /clr), a chamada para a função virtual em main
gera uma chamada para o construtor de cópia de T
e uma chamada para o destruidor. Um comportamento semelhante é obtido quando a função virtual é declarada com /clr e __clrcall
. No entanto, quando apenas compilada com /clr, a chamada de função gera uma chamada para o construtor de cópia, mas há outra chamada para o construtor de cópia devido à conversão de nativo para gerenciado.
Código
// 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");
}
Saída de exemplo
__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)
Exemplo: efeito da conversão dupla
Descrição
O exemplo anterior demonstrou a existência de uma conversão dupla. Este exemplo mostra o efeito dela. O loop for
chama a função virtual e o programa relata o tempo de execução. O tempo mais lento é relatado quando o programa é compilado com /clr. Os tempos mais rápidos são relatados ao compilar sem /clr ou se a função virtual é declarada com __clrcall
.
Código
// 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");
}
Saída de exemplo
4.2 seconds
after calling struct S