2011-01-26 86 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)。即使那樣,你也許可以提供一個功能來同時獲取和設置很多東西,從而最大限度地降低成本。人們擔心虛擬調度的方式太多 - 所以在找到彆扭的方案之前也要進行剖析。它們的主要問題是它們執行一個外聯函數調用,儘管它們也會移除執行的代碼,這會改變緩存利用率模式(爲了更好或更常見)。