我想從一個遊戲程序員的角度探討OOP的一個問題──性能。
現時C++可以說是支持OOP範式中最為常用及高性能的語言。雖然如此,在C++使用OOP的編程方式在一些場合未能提供最高性能。[1]詳細描述了這個觀點,我在此嘗試簡單說明。注意:其他支持OOP的語言通常都會有本答案中提及的問題,C++只是一個合適的說明例子。
歷史上,OOP大概是60年代出現,而C++誕生於70年代末。現在的硬件和當時的有很大差異,其中最大的問題是内存墙_百度百科。
圖1: 處理器和內存的性能提升比較,處理器的提升速度大幅高於內存 [2]。
跟據Numbers Every Programmer Should Know By Year:
圖2:2014年計算機幾種操作的潛伏期(latency)。
從這些數據,我們可以看出,內存存取成為現代計算機性能的重要瓶頸。然而,這個問題在C++設計OOP編程範式的實現方式之初應該並未能考慮得到。現時的OOP編程有可能不緩存友好(cache friendly),導致有時候並不能發揮硬件最佳性能。以下描述一些箇中原因。
1. 過度封裝
使用OOP時,會把一些複雜的問題分拆抽象成較簡單的獨立對象,通過對象的互相調用去實現方案。但是,由於對象包含自己封裝的數據,一個問題的數據集會被分散在不同的內存區域。互相調用時很可能會出數據的cache miss的情況。
2. 多態
在C++的一般的多態實現中,會使用到虛函數表。虛函數表是通過加入一次間接層來實現動態派送。但在調用的時候需要讀取虛函數表,增加cache miss的可能性。基本上要支持動態派送,無論用虛函數表、函數指針都會形成這個問題,但如果類的數目極多,把函數指針如果和數據放在一起有時候可放緩問題。
3. 數據布局
雖然OOP本身並無限制數據的布局方式,但基本上絕大部分OOP語言都是把成員變量連續包裹在一段內存中。甚至使用C去編程的時候,也通常會使用到OOP或Object-based的思考方式,把一些相關的數據放置於一個struct之內:
struct Particle {
Vector3 position;
Vector4 velocity;
Vector4 color;
float age;
// ...
};
即使不使用多態,我們幾乎不加思索地會使用這種數據布局方式。我們通常會以為,由於各個成員變量都緊湊地放置在一起,這種數據布局通常對緩存友好。然而,實際上,我們需要考慮數據的存取模式(access pattern)。
在OOP中,通過封裝,一個類的各種功能會被實現為多個成員函數,而每個成員函數實際上可能只會存取少量的成員變量。這可能形式非常嚴重的問題,例如:
for (Particle* p = begin; p != end; ++p)
p->position += p->velocity * dt; // 或 p->SimulateMotion(dt);
在這種模式下,實階上只存取了兩個成員變量,但其他成員變量也會載入緩存造成浪費。當然,如果在迭代的時候能存取盡量多的成員變量,這個問題可能並不存在,但實際上是很困難的。
如果採用傳統的OOP編程範式及實現方式,數據布局的問題幾乎沒有解決方案。所以在[1]裡,作者提出,在某些情況下,應該放棄OOP方式,以數據的存取及布局為編程的考慮重中,稱作面向數據編程(data-oriented programming, DOP)。
有關DOP的內容就不在此展開了,讀者可參考[1],還有[3]作為實際應用例子。
[1] ALBRECHT, “Pitfalls of Object Oriented Programming”, GCAP Australia, 2009. http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf
[2] Hennessy, John L., and David A. Patterson. Computer architecture: a quantitative approach. Elsevier, 2012.
[3] COLLIN, “Culling the Battlefield”, GDC 2011. http://dice.se/wp-content/uploads/CullingTheBattlefield.pdf
— 完 —
本文作者:Milo Yip
【知乎日报】
你都看到这啦,快来点我嘛 Σ(▼□▼メ)
此问题还有 65 个回答,查看全部。
延伸阅读:
面向对象编程的重要性在哪?
怎样才是纯面向对象编程语言?