提高C++性能的编程技术笔记:总结
《提高C++性能的編程技術》這本書是2011年出版的,書中有些內容的介紹可能已經過時,已不再適用于現在的C++編程中,但大部分內容還是很有參考意義的。
這里是基于之前所有筆記的簡單總結,筆記列表如下:
跟蹤實例:https://blog.csdn.net/fengbingchun/article/details/83449625
構造函數和析構函數:https://blog.csdn.net/fengbingchun/article/details/83960568
虛函數、返回值優化:https://blog.csdn.net/fengbingchun/article/details/84185474
臨時對象:https://blog.csdn.net/fengbingchun/article/details/84192535
單線程內存池:https://blog.csdn.net/fengbingchun/article/details/84497625
多線程內存池:https://blog.csdn.net/fengbingchun/article/details/84592548
內聯:https://blog.csdn.net/fengbingchun/article/details/85030305
標準模板庫:https://blog.csdn.net/fengbingchun/article/details/85470197
引用計數:https://blog.csdn.net/fengbingchun/article/details/85861776
編碼優化:https://blog.csdn.net/fengbingchun/article/details/85934251
設計優化、可擴展性、系統體系結構相關:https://blog.csdn.net/fengbingchun/article/details/86549506
跟蹤實例:最理想的跟蹤性能優化的方法應該能夠完全消除性能開銷,即把跟蹤調用嵌入在#ifdef塊內。使用這種方法的不足在于必須重新編譯程序來打開或關閉跟蹤。
影響C++性能的因素:I/O的開銷是高昂的;函數調用的開銷是要考慮的一個因素,因此我們應該將短小的、頻繁調用的函數內聯;復制對象的開銷是高昂的,最好選擇傳遞引用,而不是傳遞值。
內聯對大塊頭函數的影響是無足輕重的。只有在針對那些調用和返回開銷占全部開銷的絕大部分的小型函數時,內聯對性能的改善才有較大的影響。內聯消除了常被使用的小函數調用所產生的函數開銷。
對象定義會觸發隱形地執行構造函數和析構函數。我們稱其為”隱性執行”而不是”隱性開銷”是因為對象的構造和銷毀并不總是意味產生開銷。如果構造函數和析構函數所執行的計算是必須的,那么就要考慮使用高效的代碼(內聯會減少函數調用和返回的開銷)。
通過引用傳遞對象還是不能保證良好的性能,所以避免對象的復制的確有利于提高性能,但是如果我們不必一開始就創建和銷毀該對象的話,這種處理方式將更有利于性能的提升。
被打算創造設計靈活性的世界紀錄。你的設計只需要在當前問題范圍之內足夠靈活就可以了。在完成同樣的簡單工作時,char指針有時可以比string對象更有效率。
內聯消除了常被使用的小函數調用所產生的函數開銷。
構造函數和析構函數:對象的創建和銷毀往往會造成性能的損失。在繼承層次中,對象的創建將引起其先輩的創建。對象的銷毀也是如此。
構造函數和析構函數可以像手工編寫的C代碼一樣有效。然而在實踐中,它們經常包含冗余計算。
性能優化經常需要犧牲一些其它軟件目標,諸如靈活性、可維護性、成本和重用之類的重要目標經常必須為性能讓步。
對象的創建(或銷毀)觸發對父對象和成員對象的遞歸創建(或銷毀)。要當心復雜層次中對象的復合使用。它們使得創建和銷毀的開銷更為高昂。
在C++中,不自覺地在程序開始處預先定義所有對象的做法是一種浪費。因為這樣可能會創建一些直到最后都沒有用到的對象。在C++中,把變量的創建延遲到第一次使用前。
要確保所編寫的代碼實際使用了所有創建的對象和這些對象所執行的計算。有的應用程序注重可維護性和靈活性,而另一些應用程序則可能把對性能的考慮放在最為重要的位置。作為程序員,應當清楚自己到底更看重哪個方面。
對象的生命周期不是無償的。至少對象的創建和銷毀會消耗CPU周期。不要隨意創建一個對象,除非你打算使用它。通常情況下,要等到需要使用對象的地方再創建它。
編譯器必須初始化被包含的成員對象之后再執行構造函數體。你必須在初始化階段完成成員對象的創建。這可以降低隨后在構造函數部分調用賦值操作符的開銷。在某些情況下,這樣也可以避免臨時對象的產生。
虛函數:在以下幾個方面,虛函數可能會造成性能損失:構造函數必須初始化vptr(虛函數表);虛函數是通過指針間接調用的,所以必須先得到指向虛函數表的指針,然后再獲得正確的函數偏移量;內聯是在編譯時決定的,編譯器不可能把運行時才解析的虛函數設置為內聯。
某些情況下,在編譯期間解析虛函數的調用是可能的,但這是例外情況。由于在編譯期間不能確定所調用的函數所屬的對象類型,所以大多數虛函數調用都是在運行期間解析的。編譯期間無法解析對內聯造成了負面影響。由于內聯是在編譯期間確定的,所以它需要具體函數的信息,但如果在編譯期間不能確定將調用哪個函數,就無法使用內聯。
評估虛函數的性能損失就是評估無法內聯該函數所造成的損失。這種損失的代價并不固定,它取決于函數的復雜程度和調用頻率。一種極端情況是頻繁調用的簡單函數,它們是內聯的最大受益者,若無法內聯則會造成重大性能損失。另一極端情況是很少調用的復雜函數。
虛函數的代價在于無法內聯函數調用,因為這些調用是在運行時動態綁定的。唯一潛在的效率問題是從內聯獲得的速度(如果可以內聯的話)。但對于那些代價并非取決于調用和返回開銷的函數來說,內聯效率不是問題。
模板比繼承提供更好的性能。它把對類型的解析提前到編譯期間,我們認為這是沒有成本的。
函數返回值:通過轉換源代碼和消除對象的創建來加快源代碼的執行速度,這種優化稱為返回值優化(Return Value Optimization, RVO)。
如果必須按值返回對象,通過RVO可以省去創建和銷毀局部對象的步驟,從而改善性能。
RVO的應用要遵照編譯器的實現而定。這需要參考編譯器文檔或通過實驗來判斷是否使用RVO以及何時使用。
通過編寫計算性構造函數可以更好地使用RVO。
臨時對象:臨時對象會以構造函數和析構函數的形式降低一半的性能。
將構造函數聲明為explicit,可以阻止編譯器在幕后使用類型轉換。
編譯器常常創建臨時對象來解決類型不匹配問題。通過函數重載可以避免這種情況。
如果可能,應盡量避免使用對象拷貝。按引用傳遞和返回對象。
在<op>可能是”+、-、*”或者”/”的地方,使用<op>=運算符可以消除臨時對象。
單線程內存池:頻繁地分配和回收內存會嚴重地降低程序的性能。性能降低的原因在于默認的內存管理是通用的。應用程序可能會以某種特定的方式使用內存,并且為不需要的功能付出性能上的代價。通過開發專用的內存管理器可以解決這個問題。對專用內存管理器的設計可以從多個角度考慮。我們至少可以想到兩個方面:大小和并發。
從大小的角度分為以下兩種:
(1)、固定大小:分配固定大小內存塊的內存管理器。
(2)、可變大小:分配任意大小內存塊的內存管理器。所請求分配的大小事先是未知的。
類似的,從并發的角度也分為以下兩種:
(1)、單線程:內存管理器局限在一個線程內。內存被一個線程使用,并且不越出該線程的界限。這種內存管理器不涉及相互訪問的多線程。
(2)、多線程:內存管理器被多個線程并發地使用。這種實現需要包含互斥執行的代碼段。無論什么時候,只能有一個線程在執行一個代碼段。
靈活性以速度的降低為代價.隨著內存管理的功能和靈活性的增強,執行速度將降低.
全局內存管理器(由new()和delete()執行)是通用的,因此代價高。
專用內存管理器比全局內存管理器快一個數量級以上。
如果主要分配固定大小的內存塊,專用的固定大小內存管理器將明顯地提升性能。
如果主要分配限于單線程的內存塊,那么內存管理器也會有類似的性能提高。由于省去了全局函數new()和delete()必須處理的并發問題,單線程內存管理器的性能有所提高。
多線程內存池:全局內存管理器(通過new()和delete()實現)是通用的,因此它的開銷也非常大。
因為單線程內存管理器要比多線程內存管理器快的多,所以如果要分配的大多數內存塊限于單線程中使用,那么可以顯著提升性能。
如果開發了一套有效的單線程分配器,那么通過模板可以方便地將它們擴展到多線程環境中。
內聯基礎:內聯類似于宏,在調用方法內部展開被調用方法,以此來代替方法的調用。一般來說表達內聯意圖的方式有兩種:一種是在定義方法時添加內聯保留字的前綴;另一種是在類的頭部聲明中定義方法。
從邏輯上說,編譯器將方法內聯化的步驟如下:首先將待內聯方法的連續代碼塊復制到調用方法中的調用點處。然后在塊中為所有內聯方法的局部變量分配內存。之后將內聯方法的輸入參數和返回值映射到調用方法的局部變量空間內。最后,如果內聯方法有多個返回點,將其轉變為內聯代碼塊末尾的分支。經過這樣的處理即可消除所有與調用相關的痕跡以及性能損失。避免方法調用僅僅是內聯可提升的性能空間的一半。調用間(cross-call)優化是內聯可提升的性能空間的另外一半。優秀的、經過優化的編譯器可以使內聯方法的邊界痕跡難以區分。方法中大量的甚至是所有的代碼經過優化后都將不復存在,因為編譯器可能會對方法中大部分代碼進行重新排列。因此,盡管在邏輯上可以將方法內聯化看作是對一定內聚度的維持,不過編譯器并未強制執行這種優化措施,這也是內聯的優點之一。
如果方法有返回值,特別當其返回值是一個對象時,被調用方法將對象復制到調用方法為返回值預留的存儲空間中也是一筆開銷。對于較大的對象而言,這筆額外開銷將更加客觀,尤其是使用復雜的拷貝構造函數執行該任務時(這種會產生兩份調用/返回開銷:一份開銷是因為對方法的顯示調用,另一份則是拷貝構造函數返回一個對象時產生的開銷)。
內聯是一種由編譯器/配置器/優化器執行的、基于編譯和配置的優化操作。
保留字”inline”僅表示對編譯器的一種建議。它告訴編譯器,將方法代碼內聯展開而不是調用可以獲得更佳性能。但是編譯器沒有義務答應內聯請求。因此編譯器可以根據自己的意愿或者能力來選擇是否進行內聯。這就意味著即使沒有被明確告知需要內聯(對低價值方法編譯器會自動內聯,這常常是優化的副作用)編譯器也會這樣去做,或者即便被明確告知需要內聯卻不進行內聯。
內聯還會引起一些值得注意的副作用:從邏輯上來說,雖然經常被存放于單獨的.inl文件中,但其實內聯方法的定義應為類頭文件的一部分。頭文件及其邏輯上包含的.inl文件隨后被用到它們的.c或.cpp文件包含。源文件被編譯為目標文件后,就不需要在目標文件做任何標示以說明目標文件包含哪些內聯方法了。也就是說,通常情況下,目標文件已完全解析了內聯方法且不需要再對其存在性進行保存(不存在鏈接需求)。因此,盡管C++語言明文禁止,但是源文件仍然可以和內聯方法的定義一起編譯,而另一源文件也可以和另一版本不同但方法相同的文件一起編譯。
如果編譯器足夠完善,許多對虛方法的調用是可以內聯化的。因此,如果配置文件指出某些虛方法需要占用程序過多的運行時間,則可通過將部分方法調用內聯化來挽回一些開銷。這也說明如果編譯器有能力并且選擇了將虛方法內聯化,那么幾乎可以保證一定會有一些針對同一方法的內聯調用實例以及虛方法調用實例。
內聯就是用方法的代碼來替換對方法的調用。
內聯通過消除調用開銷來提升性能,并且允許進行調用間優化。
內聯的主要作用是對運行時間進行優化,當然它也可以使可執行映像變得更小。
內聯----站在性能的角度:調用間(cross-call)優化:面向某一方法的調用過程,基于對上下文場景更加全面的理解,使得編譯器在源代碼層面及機器代碼層面對方法進行優化。這種優化的一般形式為:在編譯期間進行一部分預處理,從而避免在運行時重復類似的過程。內聯的這類優化應是編譯器的職責,而不是程序員的。
何時避免內聯:當程序中所有能夠內聯的方法都進行內聯,代碼膨脹將不可估量,這將對性能產生巨大的二次負面影響,如緩存命中問題和頁面錯誤,而這些將令我們的工作得不償失。另一方面,濫用的內聯程序將執行較少的指令,但會耗費較多的時鐘周期。內聯的濫用導致的緩存錯誤會使性能銳減。代碼膨脹所帶來的副作用可能是無法承受的。
通常應避免遞歸方法內聯。
編譯器通常禁止內聯復雜的方法。
直接量參數與內聯結合使用,為編譯器性能的大幅提升開辟了更為廣闊的空間。
使用內聯有時會適得其反,尤其是濫用的情況下,內聯可能會使代碼量變大,而代碼量增多后會較原先出現更多的緩存失敗和頁面錯誤。
非精簡方法的內聯決策應根據樣本執行的配置文件來制定,不能主觀臆斷。
對于那些調用頻率高的方法,如果其靜態尺寸較大,而動態尺寸較小,可以考慮將其重寫,從而抽取其核心的動態特性,并將動態組件內聯。
精簡化與唯一化方法總是可以被內聯。
內聯技巧:內聯可以改善性能。目標是找到程序的快速路徑,然后內聯它,盡管內聯這個路徑可能要費點工夫。
條件內聯可以阻止內聯的發生。這樣就減少了編譯時間,同時也簡化了開發前期的調試工作。
選擇性內聯是一種只在某些地方內聯方法的技術。在對方法進行內聯時,為了抵消可能的代碼尺寸膨脹的影響,選擇性內聯只在對性能有重大影響的路徑上對方法調用進行內聯。
遞歸內聯是一種讓人感覺別扭,但對于改善遞歸方法性能卻很有效的技術。
內聯的目標是消除調用開銷。在使用內聯之前須先弄清當前系統中真正的調用代價。
標準模板庫:標準模板庫(Standard Template Library, STL)是容器和通用算法的強效組合。
STL實現在以下幾個方面形成了自己的優勢:(1). STL實現使用最好的算法;(2). STL實現的設計者非常有可能是領域內的專家;(3). 這些領域內的專家完全地致力于提供一個靈活、強大并且高效的庫。這是他們的首要任務。
STL是抽象、靈活性和效率的一種罕見的結合。對于某種特定的應用模式,一些容器比其它的更加高效,這都要隨著實際應用而定。除非了解一些相關領域內STL所忽略的問題,否則你是不可能超過STL實現的。不過,在一些特定的情況下,還是有可能超越STL實現的性能的。
引用計數:基本思想是將銷毀對象的職責從客戶端代碼轉移到對象本身。對象跟蹤記錄自身當前被引用的數目,在引用計數達到零時自行銷毀。換句話說,對象不再被使用時自行銷毀。
引用計數在性能上并非無往不勝。引用計數、執行時間和資源維護會產生微妙的相互作用,如果對性能方面的考慮很重要,就必須對這幾個方面仔細進行評估。引用計數是提升還是損害性能取決于其使用方式。下面的任意一種情況都可以使引用計數變得更為有效:目標對象是很大的資源消費者;資源分配和釋放的代價很高;高度共享,由于使用賦值操作符和拷貝構造函數,引用計數的性能可能會很高;創建和銷毀引用的代價低廉。反之,則應跳出引用計數而轉為使用更加有效的簡單非計數對象。
編碼優化:編碼優化在范圍上是局部的,并且不需要對程序的整體設計有深入的理解。當你加入到一個正在進行的開發項目中,并且你對其設計還沒有完全理解時,這會是一個很好的起點。
最快的代碼是從不執行的代碼。試著按照以下步驟去剔除那些代價高昂的計算:
(1). 你打算使用該計算結果嗎?聽起來有點可笑,但這種可笑的事確實會發生----有時我們執行了計算但從未使用計算的結果。
(2). 你現在需要該結果嗎?請在真正需要的時候再進行計算。在一些執行流程中有些結果永遠不會被使用,因此不必過早地計算。
(3). 你是否已經知道結果?如果在程序執行流程的前期已經計算出了結果,那么應該使用該結果成為可重用的。
有的時候可能無法繞開該計算,此時就必須完成它。那么現在的挑戰就是加快計算速度:
(1). 該計算是否過于通用?你的實現只需要跟該領域要求的一樣靈活就行,而無須奢求。可以充分利用簡化的假設以降低靈活性來增加速度。
(2). 一些靈活性隱藏在函數調用中。通過實現庫調用的自定義版本可以提升速度。不過,這些庫調用必須是被頻繁調用的,否則你的努力將得不到明顯效果。熟悉你所使用的庫和系統調用中隱藏的代價。
(3). 盡量減少內存管理調用的數量。在絕大多數編譯器中,這些調用的代價都是非常高的。
(4). 如果考慮所有可能的輸入數據,則可以發現20%的數據在80%時間里出現。因此,應當以犧牲其它不經常出現的場景為代價來提高典型輸入的處理速度。
(5). 緩存、RAM和磁盤訪問的速度差異很明顯。應該多編寫緩存友好的代碼。
設計優化:我們可以粗略地將性能優化分為兩種類型:編碼優化和設計優化。設計優化貫穿于所有代碼。
在軟件性能和靈活性之間存在一種基本的平衡。對于在80%時間內執行的20%的軟件,性能通常損失在靈活性上。
在代碼細節中可以利用緩存優化代碼,在這個程序設計中也能采用這種方法。通常可以通過將先前的計算結果保存起來避免大量的計算。
對于軟件的高效性而言,使用高效的算法和數據結構是必要條件,但并非充分條件。
有些計算只有在特定執行條件下才需要。這些計算應該被推遲到確實需要它們的路徑上來完成。如果過早地執行計算,那么其結果可能并沒有被調用。
大型軟件往往會變得錯綜復雜,雜亂不堪。混亂軟件的一大特點就是執行失效代碼:那些曾經用來實現某個目標,但現在已經不需要的代碼。定期清理失效和僵死代碼可以增強軟件性能,同時對于軟件也是一種維護。
可擴展性:使用單個鎖來保護多個不相關的共享資源,一般來說都不是一個好主意。這樣做會擴大臨界區的范圍,并造成其它不相關的線程間沖突。此規則的唯一例外是在滿足以下兩個條件時:所有共享資源總是一起操作;任何共享資源的操作都不會消耗大量的CPU周期。緩存的原子單元以行為單位。
實現可擴展性的技巧是減少或者消除順序化的代碼。以下是可以達到這個目標的一些步驟:
任務分解:將大的任務分為小任務,使線程并發地執行這些小任務。
代碼移出:臨界區應該只包含關鍵代碼,不直接操作共享資源的代碼不要放在臨界區內。
利用緩存:有時,通過緩存之前訪問過的數據,可以消除對臨界區的訪問。
無共享:如果需要少量、數目固定的資源實例,可以不使用公共資源池。你可以把這些資源實例設為線程私有,并最后回收。
部分共享:有兩個一樣的資源池可以減少一半的競爭。
鎖粒度:不要用同樣的鎖來保護所有資源,除非這些資源是同時更新的。
偽共享:不要在類定義里把兩個使用頻度都很高的鎖放太靠近。你肯定不希望它們共享同一個緩存并觸發緩存一致性風暴。
驚群現象:仔細分析你的鎖調用的特征。當鎖被釋放時,是所有的等待線程都被喚醒還是只喚醒一個線程?喚醒所有線程會威脅到應用的可擴展性。
系統和類庫調用:考察這些調用的實現特征。它們有可能是隱藏了順序化的代碼。
讀/寫鎖:以讀為主的共享數據會從這種鎖中獲益,使用這種鎖,可以消除讀者線程之間的競爭。
系統體系結構相關話題:存儲器層級從最快(訪問時間最短)到最慢(訪問時間最長)其排序為:寄存器、L1(第一級)芯片內緩存、L2(第二級)芯片外緩存、主存(半導體動態隨機訪問內存:DRAM、SDRAM、RAMBUS、SyncLink等等)、磁盤存儲器。
寄存器:存儲器之王。在存儲器層級上的所有實體中,寄存器延遲最短,帶寬最高,開銷最小。寄存器可由機器代碼直接尋址。正確地使用寄存器存放變量可以使某些編譯器產生的個別方法在性能上大幅提升。
最快的代碼是直線型的代碼:沒有條件判斷,沒有循環,沒有調用,沒有返回。一般來說,程序的關鍵路徑越像一條直線,執行速度就越快。請記住:短小而帶有很多分支的代碼要比長而沒有分支的代碼所用的執行時間長。
使用簡單計算代替小分支:分支是性能的敵人。
多線程是一種相當有用的機制,而且在合適的系統中能帶來巨大的性能優勢,但錯誤地使用線程概念會導致嚴重的性能問題。
要使用的存儲器離處理器越遠,訪問所需的時間就越長。離處理器最近的是寄存器,雖然容量很少,但是速度很快。對寄存器的優化對程序的性能提升而言是極其有意的。
虛擬存儲器并不是無償的,不加選擇地依賴系統管理的虛擬結構可能會影響性能,而且一般都是降低性能。
上下文切換的開銷巨大,需避免上下文切換。
GitHub:?https://github.com/fengbingchun/Messy_Test?
總結
以上是生活随笔為你收集整理的提高C++性能的编程技术笔记:总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 提高C++性能的编程技术笔记:设计优化/
- 下一篇: 用python3实现指定目录下文件sha