2011-01-26 91 views
7

所以,假设我想创建一系列类,每个类都有一个具有相同内容的成员函数。让我们调用函数避免虚函数

void doYourJob(); 

我想最终把所有这些类在同一个容器中,让我可以遍历他们有各自执行“doYourJob()”

显而易见的解决办法是让一个抽象类与功能

virtual void doYourJob(); 

但我很犹豫要这样做。这是一个耗时的程序,虚拟功能会大大增加它的价值。而且,这个函数是这些类唯一的共同点,每个类对doYourJob完全不同。

有没有办法避免使用具有虚拟功能的抽象类,或者我将不得不把它吸了?

+1

使用多态类实现它。调度虚拟功能的开销很可能是微不足道的。最糟糕的情况是,这很重要,而且你至少会有一个干净的设计,可以相对容易地进行优化。 – 2011-01-26 06:25:56

回答

6

虚拟功能不会花费太多。它们是一个间接调用,基本上就像一个函数指针。 What is the performance cost of having a virtual method in a C++ class?

如果你在每次调用计数每个周期,也就是你在函数调用做的工作非常少的情况下,你会从你的内部循环的性能是至关重要的应用程序,你可能叫它完全需要一种不同的方法。

4

恐怕一个循环中的一系列dynamic_cast检查会使性能比虚拟函数更差。如果你打算把它们都放在一个容器中,它们需要有一些共同的类型,所以你可以使它成为一个纯虚拟的基类。

在这种情况下虚拟功能调度并没有太多:虚表查找,提供的指针调整和间接调用。

如果性能至关重要,您可以为每个子类型使用单独的容器并独立处理每个容器。如果顺序很重要,你会做很多后空翻,虚拟调度可能会更快。

+3

`dynamic_cast`只适用于至少有一个虚拟函数的类型,如果是这种情况,那么使用`doYourJob()`这个虚函数会更好。 – templatetypedef 2011-01-26 06:20:06

+0

会像使用[code] class base_class {virtual void doYourJob(){}} [code]一样简单吗?或者有什么方法可以使它更高效 – hedgehogrider 2011-01-26 06:23:10

1

如果您打算将所有这些对象存储在同一个容器中,那么要么您将不得不编写异构容器类型(缓慢且昂贵),您将不得不存储容器(yuck!),或者这些类将不得不通过继承相互关联。如果您选择使用前两种选项中的任何一种,则必须具备一些逻辑以查看容器中的每个元素,找出它的类型,然后调用适当的实现,该实现基本上是沸腾的一直到继承。

我强烈建议尝试使用继承的简单,直接的方法。如果这足够快,那太棒了!你完成了。如果不是,则尝试使用其他方案。除非您有很好的证据证明成本太高,否则不要因为成本而避免使用有用的语言功能。

8

如果您需要速度,请考虑在对象中嵌入“type(-identifying)number”,并使用switch语句选择类型特定的代码。这可以完全避免函数调用开销 - 只是进行本地跳转。你不会比这更快。成本(就可维护性,重新编译依赖性等而言)是强制特定类型功能的本地化(在交换机中)。


实现

#include <iostream> 
#include <vector> 

// virtual dispatch model... 

struct Base 
{ 
    virtual int f() const { return 1; } 
}; 

struct Derived : Base 
{ 
    virtual int f() const { return 2; } 
}; 

// alternative: member variable encodes runtime type... 

struct Type 
{ 
    Type(int type) : type_(type) { } 
    int type_; 
}; 

struct A : Type 
{ 
    A() : Type(1) { } 
    int f() const { return 1; } 
}; 

struct B : Type 
{ 
    B() : Type(2) { } 
    int f() const { return 2; } 
}; 

struct Timer 
{ 
    Timer() { clock_gettime(CLOCK_MONOTONIC, &from); } 
    struct timespec from; 
    double elapsed() const 
    { 
     struct timespec to; 
     clock_gettime(CLOCK_MONOTONIC, &to); 
     return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec); 
    } 
}; 

int main(int argc) 
{ 
    for (int j = 0; j < 3; ++j) 
    { 
    typedef std::vector<Base*> V; 
    V v; 

    for (int i = 0; i < 1000; ++i) 
     v.push_back(i % 2 ? new Base : (Base*)new Derived); 

    int total = 0; 

    Timer tv; 

    for (int i = 0; i < 100000; ++i) 
     for (V::const_iterator i = v.begin(); i != v.end(); ++i) 
      total += (*i)->f(); 

    double tve = tv.elapsed(); 

    std::cout << "virtual dispatch: " << total << ' ' << tve << '\n'; 

    // ---------------------------- 

    typedef std::vector<Type*> W; 
    W w; 

    for (int i = 0; i < 1000; ++i) 
     w.push_back(i % 2 ? (Type*)new A : (Type*)new B); 

    total = 0; 

    Timer tw; 

    for (int i = 0; i < 100000; ++i) 
     for (W::const_iterator i = w.begin(); i != w.end(); ++i) 
     { 
      if ((*i)->type_ == 1) 
       total += ((A*)(*i))->f(); 
      else 
       total += ((B*)(*i))->f(); 
     } 

    double twe = tw.elapsed(); 

    std::cout << "switched: " << total << ' ' << twe << '\n'; 

    // ---------------------------- 

    total = 0; 

    Timer tw2; 

    for (int i = 0; i < 100000; ++i) 
     for (W::const_iterator i = w.begin(); i != w.end(); ++i) 
      total += (*i)->type_; 

    double tw2e = tw2.elapsed(); 

    std::cout << "overheads: " << total << ' ' << tw2e << '\n'; 
    } 
} 

性能结果

在我的Linux系统:

~/dev g++ -O2 -o vdt vdt.cc -lrt 
~/dev ./vdt      
virtual dispatch: 150000000 1.28025 
switched: 150000000 0.344314 
overhead: 150000000 0.229018 
virtual dispatch: 150000000 1.285 
switched: 150000000 0.345367 
overhead: 150000000 0.231051 
virtual dispatch: 150000000 1.28969 
switched: 150000000 0.345876 
overhead: 150000000 0.230726 

这表明联机型号码切换方法的速度约为(1.28 - 0.23)/(0.344 - 0.23)= 9.2倍。当然,这只针对确切的系统测试/编译器标志&版本等,但通常是指示性的。


评论RE虚拟监控调度

必须说,虽然虚拟函数调用的开销是东西是很少显著,并且只适用于经常被称为琐碎的功能(如getter和setter)。即使那样,你也许可以提供一个功能来同时获取和设置很多东西,从而最大限度地降低成本。人们担心虚拟调度的方式太多 - 所以在找到别扭的方案之前也要进行剖析。它们的主要问题是它们执行一个外联函数调用,尽管它们也会移除执行的代码,这会改变缓存利用率模式(为了更好或更常见)。