[英]Virtual Functions and Performance C++
在你對重復的標題感到畏縮之前,另一個問題不適合我在這里提出的要求(IMO)。 所以。
我真的想在我的應用程序中使用虛函數使事情變得容易一百倍(不是OOP的全部內容;))。 但是我讀到了他們以性能成本出現的某個地方,只看到過早優化的同樣過時的炒作炒作,我決定在一個小的基准測試中快速調整它:
CProfiler.cpp
#include "CProfiler.h"
CProfiler::CProfiler(void (*func)(void), unsigned int iterations) {
gettimeofday(&a, 0);
for (;iterations > 0; iterations --) {
func();
}
gettimeofday(&b, 0);
result = (b.tv_sec * (unsigned int)1e6 + b.tv_usec) - (a.tv_sec * (unsigned int)1e6 + a.tv_usec);
};
main.cpp中
#include "CProfiler.h"
#include <iostream>
class CC {
protected:
int width, height, area;
};
class VCC {
protected:
int width, height, area;
public:
virtual void set_area () {}
};
class CS: public CC {
public:
void set_area () { area = width * height; }
};
class VCS: public VCC {
public:
void set_area () { area = width * height; }
};
void profileNonVirtual() {
CS *abc = new CS;
abc->set_area();
delete abc;
}
void profileVirtual() {
VCS *abc = new VCS;
abc->set_area();
delete abc;
}
int main() {
int iterations = 5000;
CProfiler prf2(&profileNonVirtual, iterations);
CProfiler prf(&profileVirtual, iterations);
std::cout << prf.result;
std::cout << "\n";
std::cout << prf2.result;
return 0;
}
起初我只進行了100次和10000次迭代,結果令人擔憂:非虛擬化為4ms,虛擬化為250ms! 我幾乎在里面“nooooooo”,但隨后我將迭代次數增加到500,000左右; 看到結果幾乎完全相同(如果沒有啟用優化標志,可能會慢5%)。
我的問題是,與高金額相比,為什么迭代量較少會出現如此顯着的變化? 是純粹因為在那么多次迭代中虛擬函數在緩存中很熱嗎?
放棄
我理解我的“分析”代碼並不完美,但它實際上給出了對事物的估計,這在這個層面上是最重要的。 我也要問這些問題,而不是僅僅優化我的應用程序。
我相信你的測試用例過於人為,不具備任何重要價值。
首先,在您的profiled函數中,您可以動態地分配和釋放一個對象以及調用一個函數,如果您想要只調用函數調用,那么您應該這樣做。
其次,您沒有分析虛擬函數調用代表給定問題的可行替代方案的情況。 虛函數調用提供動態調度。 您應該嘗試分析一個案例,例如使用虛擬函數調用作為使用開關類型反模式的東西的替代。
擴展查爾斯的回答 。
這里的問題是你的循環不只是測試虛擬調用本身(內存分配可能使虛擬調用開銷相形見絀),所以他的建議是更改代碼,以便只測試虛擬調用。
這里的基准函數是模板,因為模板可能是內聯的,而通過函數指針調用則不太可能。
template <typename Type>
double benchmark(Type const& t, size_t iterations)
{
timeval a, b;
gettimeofday(&a, 0);
for (;iterations > 0; --iterations) {
t.getArea();
}
gettimeofday(&b, 0);
return (b.tv_sec * (unsigned int)1e6 + b.tv_usec) -
(a.tv_sec * (unsigned int)1e6 + a.tv_usec);
}
類別:
struct Regular
{
Regular(size_t w, size_t h): _width(w), _height(h) {}
size_t getArea() const;
size_t _width;
size_t _height;
};
// The following line in another translation unit
// to avoid inlining
size_t Regular::getArea() const { return _width * _height; }
struct Base
{
Base(size_t w, size_t h): _width(w), _height(h) {}
virtual size_t getArea() const = 0;
size_t _width;
size_t _height;
};
struct Derived: Base
{
Derived(size_t w, size_t h): Base(w, h) {}
virtual size_t getArea() const;
};
// The following two functions in another translation unit
// to avoid inlining
size_t Derived::getArea() const { return _width * _height; }
std::auto_ptr<Base> generateDerived()
{
return std::auto_ptr<Base>(new Derived(3,7));
}
並測量:
int main(int argc, char* argv[])
{
if (argc != 2) {
std::cerr << "Usage: %prog iterations\n";
return 1;
}
Regular regular(3, 7);
std::auto_ptr<Base> derived = generateDerived();
double regTime = benchmark<Regular>(regular, atoi(argv[1]));
double derTime = benchmark<Base>(*derived, atoi(argv[1]));
std::cout << "Regular: " << regTime << "\nDerived: " << derTime << "\n";
return 0;
}
注意:與常規函數相比,這會測試虛擬調用的開銷。 功能是不同的(因為在第二種情況下你沒有運行時調度),但它是最壞情況的開銷。
編輯 :
運行結果(gcc.3.4.2,-O2,SLES10四核服務器) 注意:使用另一個轉換單元中的函數定義,以防止內聯
> ./test 5000000
Regular: 17041
Derived: 17194
不是很有說服力。
通過少量迭代,您的代碼有可能被搶占,其他程序並行運行或交換發生或任何其他操作系統隔離您的程序,您將有時間被包含在其中的操作系統暫停你的基准測試結果 這是為什么你應該運行你的代碼十幾萬次來測量任何或多或少可靠的東西的第一個原因。
我認為這種測試實際上是無用的,事實上:
1)你正在浪費時間來調用自己調用gettimeofday()
;
2)你真的沒有測試虛函數,恕我直言這是最糟糕的事情。
為什么? 因為您使用虛函數來避免編寫如下內容:
<pseudocode>
switch typeof(object) {
case ClassA: functionA(object);
case ClassB: functionB(object);
case ClassC: functionC(object);
}
</pseudocode>
在這段代碼中,你會錯過“if ... else”塊,所以你並沒有真正獲得虛函數的優勢。 這種情況下,他們總是“失敗”對抗非虛擬。
要進行適當的分析,我認為你應該添加類似我發布的代碼。
時間差異可能有幾個原因。
堆管理器可能會影響結果,因為sizeof(VCS) > sizeof(VS)
。 如果將new
/ delete
移出循環會發生什么?
同樣,由於大小差異,內存緩存可能確實是時間差異的一部分。
但是:你應該真的比較類似的功能。 使用虛函數時,出於某種原因這樣做,即根據對象的標識調用不同的成員函數。 如果您需要此功能,並且不想使用虛函數,則必須手動實現它,無論是使用函數表還是使用switch語句。 這也需要付出代價,這就是你應該與虛擬功能進行比較的東西。
調用虛函數會對性能產生影響,因為它比調用常規函數稍微多一些。 然而,在現實世界的應用程序中,這種影響可能完全可以忽略不計 - 甚至比最精細的基准測試中出現的影響更小。
在現實世界的應用程序中,虛擬函數的替代方法通常會讓您無論如何都會手寫一些類似的系統,因為調用虛函數和調用非虛函數的行為不同 - 前者基於調用對象的運行時類型。 您的基准測試,即使忽略它具有的任何缺陷,也不會測量等效行為,只測量等效語法。 如果要制定禁止虛函數的編碼策略,則必須編寫一些可能非常迂回或混亂的代碼(可能更慢)或重新實現編譯器用於實現虛擬化的類似運行時調度系統函數行為(在大多數情況下,它肯定不會比編譯器更快)。
使用過少的迭代時,測量中會產生很多噪音。 gettimeofday
函數不夠准確,只能為少量迭代提供良好的測量,更不用說它記錄總的牆面時間(包括被其他線程搶占所花費的時間)。
但最重要的是,你不應該想出一些可笑的復雜設計來避免虛函數。 他們真的不會增加太多開銷。 如果您擁有令人難以置信的性能關鍵代碼並且您知道虛擬功能在大多數時間構成,那么可能需要擔心的事情。 但是,在任何實際應用中,虛函數都不會使您的應用程序變慢。
在我看來,當循環次數較少時,可能沒有上下文切換,但是當你增加循環次數時,那么上下文切換發生的可能性非常大,而且主導了讀取。 例如,第一個程序需要1秒,第二個程序需要3秒,但如果上下文切換需要10秒,則差值為13/11而不是3/1。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.