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

Confira também

Assemblies mistos (nativos e gerenciados)