虚拟函数与指针铸造的比较

时间:2011-07-15 22:30:32

标签: c++ oop polymorphism vtable

我正在使用的某些代码的当前版本利用了一种略微奇怪的方式来实现我认为可以通过多态实现的东西。更具体地说,我们目前使用类似

的东西
for(int i=0; i<CObjList.size(); ++i)
{
   CObj* W = CObjList[i];
   if( W->type == someTypeA )
   {
       // do some things which also involve casts such as
       //  ((SomeClassA*) W->objectptr)->someFieldA
   }
   else if( W->type == someTypeB )
   {
       // do some things which also involve casting such as
       //  ((SomeClassB*) W->objectptr)->someFieldB
   }
}

澄清;每个对象W包含一个void *objectptr;,也就是说指向任意位置的指针。字段W->type会跟踪objectptr指向的对象类型,以便在if / else语句中我们可以将W->objectptr转换为正确的类型并使用它的字段。

然而,出于几个原因,从代码设计的角度来看,这看起来本身就很糟糕;

  1. 我们无法保证W->objectptr指向的对象与W->type中的对象实际匹配,因此演员本身就不安全。
  2. 每次我们希望添加其他类型时,我们必须添加另一个elseif语句并确保W->type设置正确。
  3. 似乎用

    之类的东西可以更好地解决这个问题
    class CObj
    {
    public:
       virtual void doSomething(/* some params */)=0;
    };
    
    class SomeClassA : public CObj
    {
    public:
       virtual void doSomething(/* some params */);
       int someFieldA;
    }
    
    class SomeClassB : public CObj
    {
    public:
       virtual void doSomething(/* some params */);
       int someFieldB;
    }
    
    // sometime later...
    
    for(int i=0; i<CObjList.size(); ++i)
    {
       CObj* W = CObjList[i];
       W->doSomething(/* some params */);
    }
    

    据说这有一个附带条件,即在这种情况下表现很重要。这段代码将从(相对)紧密的循环中调用。

    我的问题是;增加的代码设计和可扩展性超过了几个vtable查找的复杂性,这可能会影响性能吗?

    编辑:我发现通过这种方式通过指针访问字段可能与vtable查找一样糟糕,因为缓存未命中等等。有什么想法吗?

    ----编辑2:我也忘了提及(我知道它有点偏离原始主题),if语句内部是对周围类的成员函数的多次调用。你将如何设计结构,以便能够从doSomething()内部调用这些?

6 个答案:

答案 0 :(得分:3)

使用虚函数,这个假设的优化意味着什么。重要的是代码可读性,可维护性和质量。

如果您确实需要调整热点,请在探查器的帮助下进行优化。使用这种垃圾使代码无法维护是一条失败的道路。

此外,虚函数将帮助您进行单元测试,模拟接口等。 编程是关于管理复杂性....

答案 1 :(得分:3)

我将特别回答性能角度,因为我在一个完全关键的环境中工作,不久之前我碰巧在类似的情况下运行测量,以找出最快的解决方案。

如果您使用的是x86,PPC或ARM处理器,则需要在这种情况下使用虚拟功能。调用虚函数的性能成本主要是由pipeline bubble引起的mispredicting an indirect branch。因为CPU的指令获取阶段无法知道计算出的jmp的去向,所以在分支执行之前它无法从目标地址开始获取字节,因此管道中的停顿对应于阶段之间的阶段数第一个获取阶段和分支退休。 (在PPC上我最了解,就像25个周期一样。)

你也有加载vtable指针的延迟,但这经常被指令重新排序隐藏(编译器移动load指令,所以它在你真正需要结果之前开始几个周期,而CPU做其他的工作而数据缓存会向你发送电子。)

使用if-cascade方法,您可以使用一些 n 直接条件分支 - 其中目标在编译时是已知的,但是是否在运行时确定是否采用了跳转。 (即,jump-on-equal操作码。)在这种情况下,CPU将猜测(预测)是否采用每个分支,并相应地开始获取指令。所以,如果CPU猜错了,你只会有一个泡沫。因为你可能每次都使用不同的输入来调用这个函数,所以它会错误地预测这些分支中的至少一个,并且你将拥有与虚拟结果完全相同的气泡。事实上,你会有更多的泡沫 - 每个if()条件一个!

使用虚函数时,还存在加载vtable时额外数据缓存未命中的风险,以及跳转目标上的icache miss。如果这个函数处于紧密循环中,那么可能你会查找并调用相同的子程序,因此vtable和函数体可能仍然在缓存中。 You could measure that如果你想要确定。

答案 2 :(得分:2)

  

我的问题是;增加的代码设计和可扩展性超过了几个vtable查找的复杂性,这可能会影响性能吗?

C ++编译器应该能够非常有效地实现虚函数,所以我认为使用它们不会有缺点。 (当然还有很大的可维护性/可读性好处!)但是你应该测量以确保。

它们通常的实现方式是每个对象都有一个vtable指针。 (在多重继承的情况下有多个指针,但现在让我们忘记了)这与非虚函数相比具有以下相对成本。

  • 数据空间:每个对象一个指针
  • 数据空间:每个类一个vtable(不是每个对象!)
  • time:worstcase =每个函数调用两次内存读取(1获取vtable地址,1获取vtable中的函数地址)。 vtable中的偏移量在编译时是已知的,因为您知道要调用的函数。没有额外的跳跃。

将此与现有软件的非OOP方法的成本进行比较。

  • 数据空间:每个对象一个类型ID
  • 代码空间:每次希望调用依赖于对象类型的函数时,一个if / else树或switch语句
  • 时间:必须评估if / else树或switch语句。

我认为虚拟函数方法实际上比非OOP方法更快,因为它消除了花时间并弄清楚它是什么类型的对象的需要。 / p>

答案 3 :(得分:2)

我有一些使用基于类似类型的开关构造的一些较大的(1M +行我认为)科学计算代码的经验。他们重构了一个适当的基于多态的方法,并获得了显着的加速。与他们的期望恰恰相反!

原来,编译器能够更好地优化该结构中的某些东西。

然而这是很久以前(8年左右)..所以谁知道现代编译器会做什么。不要猜测 - 描述它。

答案 4 :(得分:1)

正如piotr所说,正确的答案可能是虚函数。你必须测试。

但是为了解决你对演员表的担忧:

  1. 永远不要在C ++程序中使用C风格的强制转换使用static_cast&lt;&gt;,dynamic_cast&lt;&gt;等。
  2. 在您的具体情况下,请使用dynamic_cast&lt;&gt;。至少那时如果类型没有正确相关,你会得到一个例外,这比狂野的崩溃更好。

答案 5 :(得分:0)

对于此类案件,

CRTP会是一个好主意。

修改:在您的情况下,

template<class T>
class CObj
{
public:
   void doSomething(/* some params */)
   {
     static_cast<T*>(this)->doSomething(...);
   }
};

class SomeClassA : public CObj<SomeClassA>
{
public:
   void doSomething(/* some params */);
   int someFieldA;
};

class SomeClassB : public CObj<<SomeClassB>
{
public:
   void doSomething(/* some params */);
   int someFieldB;
};

现在,您可能必须以不同的方式选择循环代码,以适应不同CObj<T>类型的所有对象。