簡體   English   中英

使用成員函數指針與交換機的成本是多少?

[英]What is the cost of using a pointer to member function vs. a switch?

我有以下情況:


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

在我當前的實現中,我將whichFoo的值保存在構造函數的數據成員中,並使用callFoo()switch來決定調用哪個foo。 可替換地,我可以使用一個switch在構造函數中保存一個指針向右fooN()以調用callFoo()

我的問題是,如果A類的對象只構造一次,而callFoo()被調用了很多次,那么哪種方式更有效。 所以在第一種情況下我們有多個switch語句的執行,而在第二種情況下只有一個開關,並且使用指向它的指針多次調用成員函數。 我知道使用指針調用成員函數比直接調用它要慢。 有人知道這個開銷是多少還是少於switch的成本?

澄清:我意識到你從來沒有真正知道哪種方法可以提供更好的性能,直到你嘗試並計時。 但是,在這種情況下,我已經實施了方法1,並且我想知道方法2是否可以至少在原則上更有效。 它似乎可以,現在我有理由去實現它並嘗試它。

哦,我也喜歡方法2更好的審美原因。 我想我正在尋找實現它的理由。 :)

你是否確定通過指針調用成員函數比直接調用它更慢? 你能衡量差異嗎?

一般來說,在進行績效評估時,不應該依賴自己的直覺。 坐下來使用您的編譯器和計時功能,並實際測量不同的選擇。 你可能會感到驚訝!

更多信息:有一篇優秀的文章成員函數指針和最快可能的C ++代表 ,它們詳細介紹了成員函數指針的實現。

你可以這樣寫:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

實際函數指針的計算是線性和快速的:

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

請注意,您甚至不必在構造函數中修復實際的函數編號。

我已將此代碼與switch生成的asm進行了比較。 switch版本不提供任何性能提升。

回答問題:在最細粒度的級別,指向成員函數的指針將表現更好。

解決未提出的問題:“更好”在這里意味着什么? 在大多數情況下,我認為差異可以忽略不計。 然而,根據它所做的課程,差異可能很大。 在擔心差異之前進行性能測試顯然是正確的第一步。

如果你要繼續使用一個非常好的開關,那么你可能應該將邏輯放在一個輔助方法中並從構造函數中調用if。 或者,這是戰略模式的經典案例。 您可以創建一個名為IFoo的接口(或抽象類),它有一個帶有Foo簽名的方法。 您可以讓構造函數接受IFoo的實例(構造函數Dependancy Injection ,它實現了您想要的foo方法。您將擁有一個將使用此構造函數設置的私有IFoo,並且每次要調用Foo時,您都會調用IFoo的版本。

注意:自從大學以來我沒有使用過C ++,所以我的術語可能就在這里,對於大多數OO語言都有一般的想法。

如果您的示例是真實代碼,那么我認為您應該重新審視您的課程設計。 將值傳遞給構造函數,並使用它來改變行為實際上等同於創建子類。 考慮重構以使其更明確。 這樣做的結果是你的代碼最終會使用一個函數指針(所有虛擬方法實際上都是跳轉表中的函數指針)。

但是,如果你的代碼只是一個簡單的例子來詢問跳轉表是否比switch語句更快,那么我的直覺會說跳轉表更快,但你依賴於編譯器的優化步驟。 但是,如果性能真的是一個問題,不要依賴直覺 - 敲擊測試程序並測試它,或者查看生成的匯編程序。

有一點可以肯定,switch語句永遠不會比跳轉表慢。 原因是編譯器的優化器可以做的最好的事情是將一系列條件測試(即切換)轉換為跳轉表。 因此,如果您確實想要確定,請將編譯器從決策過程中移除並使用跳轉表。

聽起來你應該使callFoo成為一個純虛函數並創建一些A子類。

除非你真的需要速度,否則做了大量的分析和檢測,並確定callFoo的調用確實是瓶頸。 你呢?

函數指針幾乎總是比chained-ifs更好。 它們使代碼更清晰,並且幾乎總是更快(除非在它只能在兩個函數之間進行選擇並始終正確預測的情況下除外)。

我認為指針會更快。

現代CPU預取指令; 錯誤預測的分支刷新緩存,這意味着它在重新填充緩存時停止。 指針不會這樣做。

當然,你應該測量兩者。

僅在需要時進行優化

第一:大多數時候你很可能不在乎,差異會非常小。 確保首先優化此調用才有意義。 只有當您的測量顯示在呼叫開銷上花費了大量時間時,才進行優化(無恥插件 - 參見如何優化應用程序以使其更快? )如果優化不重要,則更喜歡更易讀的代碼。

間接通話費用取決於目標平台

一旦確定應用低級優化是值得的,那么就是了解目標平台的時候了。 您可以避免的成本是分支錯誤預測懲罰。 在現代的x86 / x64 CPU上,這種誤預測可能非常小(他們可以在大多數情況下很好地預測間接調用),但是當針對PowerPC或其他RISC平台時,通常根本不會預測間接調用/跳轉並避免它們可以帶來顯着的性能提升。 另請參閱虛擬呼叫成本取決於平台

編譯器也可以使用跳轉表來實現切換

一個問題:Switch有時也可以實現為間接調用(使用表),尤其是在許多可能的值之間切換時。 這種開關表現出與虛擬功能相同的誤預測。 為了使這種優化可靠,人們可能更願意使用if而不是switch來處理最常見的情況。

使用計時器來查看哪個更快。 雖然除非這段代碼一遍又一遍,否則你不太可能注意到任何差異。

如果您正在運行構造函數中的代碼,請確保如果構造失敗,則不會泄漏內存。

這種技術在Symbian OS中大量使用: http//www.titu.jyu.fi/modpa/Patterns/pattern-TwoPhaseConstruction.html

如果你只調用一次callFoo(),那么很可能函數指針的速度會慢一些。 如果你多次調用它,那么函數指針的速度可能會微不足道(因為它不需要繼續通過開關)。

無論哪種方式,看看組裝的代碼,以確定它正在做你認為它正在做的事情。

如果您知道在絕大多數情況下使用特定值,那么切換(甚至是排序和索引)的一個經常被忽視的優點是。 訂購交換機很容易,因此最先檢查最常見的交換機。

PS。 如果你關心速度測量,要加強格雷格的答案。 當CPU具有預取/預測分支和管道停頓等時,查看匯編器並沒有幫助

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM