C++:解决多态性开销问题

8

我知道多态会增加明显的开销。调用虚函数比调用非虚函数更慢。(所有我的经验都是关于GCC的,但我认为/听说这对任何真正的编译器都是正确的。)

很多时候,同一个对象上会一次又一次地调用同一个虚函数;我知道对象类型不会改变,并且大多数情况下编译器也很容易推断出来:

BaseType &obj = ...;
while( looping )
    obj.f(); // BaseType::f is virtual

为了加速代码,我可以像这样重写上面的代码:
BaseType &obj = ...;
FinalType &fo = dynamic_cast< FinalType& >( obj );
while( looping )
    fo.f(); // FinalType::f is not virtual

我想知道在这些情况下避免多态带来的开销的最佳方法是什么。
我觉得上转型的想法(如第二个片段所示)并不好:BaseType可能被许多类继承,试图将其上转型到所有这些类将会相当冗长。
另一个想法可能是将obj.f存储在函数指针中(没有测试过,不确定它是否会消耗运行时开销),但是这种方法也不完美:与上述方法一样,它需要编写更多代码,并且无法利用某些优化(例如:如果FinalType :: f是内联函数,则不会被内联--但我猜避免这种情况的唯一方法就是将obj上转型为其最终类型...)
那么,有更好的方法吗?
编辑: 好吧,当然这不会产生太大影响。这个问题主要是想知道是否有什么可以做的,因为看起来这种开销是免费的(这种开销似乎很容易消除),我不明白为什么不能这样做。
像C99的restrict关键字一样,为小优化提供一个简单的关键字,告诉编译器多态对象是固定类型的是我所希望的。
无论如何,只是回答评论,确实存在一些开销。看看这个特定的极端代码:
struct Base { virtual void f(){} };
struct Final : public Base { void f(){} };

int main( ) {
    Final final;
    Final &f = final;
    Base &b = f;

    for( int i = 0; i < 1024*1024*1024; ++ i )
#ifdef BASE
        b.f( );
#else
        f.f( );
#endif

    return 0;
}

编译和运行需要时间:

$ for OPT in {"",-O0,-O1,-O2,-O3,-Os}; do
    for DEF in {BASE,FINAL}; do
        g++ $OPT -D$DEF -o virt virt.cpp &&
        TIME="$DEF $OPT: %U" time ./virt;
    done;
  done           
BASE : 5.19                                                                                                                                                                         
FINAL : 4.21                                                                                                                                                                        
BASE -O0: 5.22                                                                                                                                                                      
FINAL -O0: 4.19                                                                                                                                                                     
BASE -O1: 3.55                                                                                                                                                                      
FINAL -O1: 1.53                                                                                                                                                                     
BASE -O2: 3.61                                                                                                                                                                      
FINAL -O2: 0.00                                                                                                                                                                     
BASE -O3: 3.58                                                                                                                                                                      
FINAL -O3: 0.00                                                                                                                                                                     
BASE -Os: 6.14                                                                                                                                                                      
FINAL -Os: 0.00

我猜只有 -O2、-O3 和 -Os 才会内联 Final::f

这些测试已经在我的机器上运行过,运行的是最新的 GCC 和 AMD Athlon(tm) 64 X2 Dual Core Processor 4000+ CPU。我猜在更便宜的平台上速度可能会慢得多。


9
你的意思是你的代码运行速度很慢,然后你进行了分析,发现问题出在多态性上,对吗? - wilhelmtell
2
如果BaseType中的f是虚函数,并且FinalType是从BaseType派生而来的,则FinalType中的f也是虚函数。 - James McNellis
1
此外,dynamic_cast<>() 在运行时需要进行检查,而多态的成本仅为单个指针解引用操作。我建议每当你说“开销”这个词时,请确保 准确地 表明此开销是什么,至少在第一次谈到这个开销时如此。这样我们就清楚我们要消除的是什么。那么,现在,我猜你对这两种方法进行了剖析,发现多态比你的 hack 更慢了? - wilhelmtell
2
如果 FinalType::f 的参数类型与 BaseType::f 的参数类型相同,则 FinalType::f 是虚函数并覆盖了 BaseType::f。在派生类中是否使用关键字 virtual 声明函数都没有关系。在您的示例中,由于 f 没有参数,因此 FinalType::f 确实覆盖了 BaseType::f - James McNellis
2
即使您知道FinalType始终是最派生的类,编译器通常很难或不可能知道这一点。虽然编译器可以优化某些虚函数调用并确定在编译时调用哪个函数,但在大多数情况下它无法做到。您的示例测试,在单个翻译单元中声明、定义和使用所有类的情况下,是编译器可以进行此优化的罕见示例。 - James McNellis
显示剩余2条评论
2个回答

8
如果动态分派在你的程序中成为了性能瓶颈,那么解决问题的方法不是使用动态分派(不要使用虚函数)。你可以使用模板和泛型编程代替虚函数,从而将一些运行时多态转换为编译时多态。这可能会带来更好的性能表现,但只有分析器才能告诉你确切的结果。需要明确的是,正如wilhelmtell在问题的评论中已经指出的那样,动态分派所带来的开销很少会对性能产生重大影响。在你放弃内置便利功能并采用笨拙的自定义实现之前,请务必确定它是你的性能热点。

1
有时候你“被迫”使用多态(因此动态分派,直到现在我才想起这个术语)。当然,你可以将多态指针/引用向上转型,然后使用它(将其传递给模板函数或其他什么);这就是我对向上转型解决方案的想法。正如在编辑原帖子后所说,这主要是关于事情如何工作的疑问。顺便说一句,感谢您的答复。 - peoro

2
如果需要使用多态性,请使用它。没有比这更快的方法。
然而,我会回答另一个问题:这是您最大的问题吗?如果是,那么您的代码已经是最优或接近最优的了。如果不是,找出最大的问题,集中精力解决它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接