C++\CLI

混合模式中函数调用的双转换(Double thunking)

2022-04-27  本文已影响0人  左图右码

使用/clr进行编译时,代码中有一个本机类(既不是ref类也不是value类),则会为其方法生成两个入口点:托管入口点和本机入口点。这允许托管调用方和本机调用方调用这些方法,并且本机入口点充当托管入口点的转换。单词 thunk(或thunking)是指从托管代码跳到本机代码(或相反)。很久以前从 Win16 编程迁移到 Win32 编程的人可能还记得从16位代码块跳转到32位函数所需的转换。此处的转换用于类似的上下文,只是跳转是在托管代码和非托管代码之间。
虚拟函数始终通过本机入口点调用,这会导致性能问题。当托管调用方调用 /clr编译的本机类中的虚拟函数时,需要两个托管/非托管转换:第一个托管到本机转换以调用本机入口点,第二个本机到托管转换以调用托管入口点。这称为双重转换。图 5.1 显示了双重转换的示意图。


image.png

让我们编写一些代码,看看双重转换的效果:

class N
{
public:
  N()
  {
    Console::WriteLine(__FUNCSIG__);
  }

  N(const N&)
  {
    Console::WriteLine(__FUNCSIG__);
  }
  ~N()
  {
    Console::WriteLine(__FUNCSIG__);
  }
};
class X
{
public:
  virtual void UseNVirt(N)
  {

    Console::WriteLine(__FUNCSIG__);
  }
}

您有一个带有构造函数、复制构造函数和析构函数的类N,并在调用函数时使用FUNCSIG宏显示函数名称。这允许您跟踪程序的执行流程。现在,让我们编写一些调用类X的UseNVirt虚拟方法的代码。为了让您看到每个相关片段的相应输出并清楚地了解程序流程,表5.1显示了代码和相应的输出。

image.png

如您所见,当调用UseNVirt时,类N的复制构造函数被调用两次。发生这种情况是因为需要两个复制构造:第一个用于调用本机子方法,第二个用于调用实际的托管方法。因此,涉及两个转换。第一个托管到本机的转换是调用本机方法,第二个从本机到托管的转换是调用托管方法。双重转换和所涉及的额外复制构造的组合会导致代码性能变差。

解决此双重问题的解决方案是指示编译器不要生成本机入口点。这是通过指定__clrcall调用约定来完成的。在方法上指定__clrcall告诉编译器该方法将仅从托管代码调用;因此,只需要生成一个托管入口点。当然,这意味着您无法从#pragma unmanaged代码块调用该方法。因此,仅当您确定调用方将始终被托管代码调用时,才应使用此调用约定。让我们向类X添加一个使用__clrcall调用约定的方法,并在测试代码中使用该函数。下面是类X的修改代码:

class X
{
public:
   virtual void UseNVirt(N)
   {
       Console::WriteLine(__FUNCSIG__);
   }
   virtual void __clrcall UseNVirtClrCall(N)
   {
       Console::WriteLine(__FUNCSIG__);
   }
};

新添加的方法的唯一区别是,指定它使用__clrcall调用约定。让我们编写一些调用此方法的代码;和以前一样,表5.2显示了相应的控制台输出。

image.png

这一次,只有一次对类 N 复制构造函数的调用。不会发生双重转换,因为只有一个入口点:托管入口点。我的建议是大量地使用__clrcall除非您确定该方法可以由本机调用方调用。请注意,VC++ 2005会自行执行一些优化,并将__clrcall隐式添加到签名中具有托管类型(包括返回类型)的任何方法中。

上一篇 下一篇

猜你喜欢

热点阅读