C++性能優化指南
這是一篇關於C++性能優化指南的學習筆記,主要是通過閱讀學習Kurt Guntheroth著的Optimized C++:Proven Techniques for Heightened Performance。 這是一本知識量和信息量很大的一本書書,書里詳細介紹了影響C++程序性能的原因,也給出了很多提高性能的優化策略。
書中不僅講解了軟件和系統方面的相關內容,還涉及了計算機的硬件組成的基礎知識,使讀者可以全面的了解計算機和程序設計。書中介紹的方法是具有通用性的,可以延伸至其他的編程語言,個人認為這是一本可以提升程序設計能力、感受到優化之美的一本值得一讀的好書。
一、C++代碼優化策略總結
1、用好的編譯器並用好編譯器(支持C++11的編譯器,IntelC++(速度最快)、GNU的C++編譯器GCC/G++(非常符合標准),Visual C++(性能折中),clang(最年輕Mac OS x))。
2、使用更好的算法。
3、使用更好的數據結構(不同的數據結構在使用內存管理器的方式也有所不同)。
4、使用更好的庫(熟悉和掌握標准C++模板庫對於進行性能優化的開發員是必須的技能,Boost Project 和 Google Code 公開了很多有用的庫)。
5、減少內存分配和復制(減少對內存管理器的調用是一種非常有效的優化手段)。
6、優化內存管理(內存管理器的調度,豐富的API)。
7、移除計算(對於單條的C++語句進行優化)。
8、提高並發性(多個處理核心執行指令)。
二、影響優化的計算機行為
1、計算機的物理組成本身對計算機性能的限制。
2、計算機的主內存是比較慢的(通往主內存的接口是限制執行速度的瓶頸(馮*諾伊曼瓶頸),(摩爾定理)每年處理器的核心的數量都會增加,但是計算機的性能未必會提高,因為這些核心只是等待訪問內存的機會(內存牆memory wall))。
3、計算機內存的訪問方式(並非以字節為單位),某些內存訪問會比其他的更慢(分為一級高速緩存(cache memory)、二級高速緩存、三級高速緩存、主內存、磁盤上的虛擬內存頁)。
4、內存的容量是有限的,每個程序都會與其他程序競爭計算機資源,計算比做決定快。
5、在處理器中,訪問內存的性能開銷遠比其他操作的性能開銷大,非對齊訪問所需要的時間是所有字節都在同一字節中的兩倍。
6、訪問頻繁使用的內存地址的速度比訪問非頻繁使用的地址快,訪問相鄰地址的內存的速度比訪問相互遠隔的地址的內存塊。
7、訪問線程間共享的數據比訪問非共享的數據資源慢很多。當並發線程共享數據時,同步代碼降低了並發量。
8、有些語句隱藏了大量的計算,從語句的外表上看不出語句的性能開銷會有多大。
三、性能測量
1、90/10規則:一個程序會花費90%的運行時去執行10%的代碼。
2、只有正確且精確的測量才是准確的測量。
3、分辨率不是准確性。
4、在Windows上,clock()函數提供了可靠的毫秒級的時鍾計時功能。在Windows8和之后的版本中,GetSystemTimePreciseAsfileTime()提供了亞微秒的計時功能。
5、計算一條C++語句對內存的讀寫次數,可以估算出一句C++ 語句的性能開銷。
四、優化字符串的使用
1、由於字符串是動態分配內存的,因此它們的性能開銷非常大。它們在表達式中的行為與值類似,它們的實現方式中需要大量的復制。
2、將字符串作為對象而非值可以降低內存分配和復制的頻率。
3、為字符串預留內存空間可以減少內存分配的開銷。
4、將指向字符串的常量引用傳遞給函數與傳遞值的結果幾乎一樣,但是更加高效。
5、將函數的結果通過輸出參數作為引用返回給調用方會復用實參的存儲空間,這可能比分配新的存儲空間更加高效。
6、即使只是有時候會減少內存分配的開銷,仍然是一種優化。
五、優化動態分配內存的變量
1、在C++程序中,亂用動態分配內存的變量是最大的“性能殺手”。
2、C++變量(每個普通數據類型的變量;每個數組,結構體或類實例)在內存中的布局都是固定的,它們的大小在編譯時就已經確定了。
3、每個變量都有它的存儲期(生命周期),只有在這段時間內變量所占用的存儲空間或者內存字節中的值才是有意義的。為變量分配內存的開銷取決於存儲期(靜態存儲期、線性局部存儲期、自動存儲期、動態存儲期)。
4、C++變量的所有者決定了變量什么時候會被創建,什么時候會被析構(變量所有權是一個單獨的概念,與存儲期不同)。動態變量的所有權必須有程序員執行並編寫在程序邏輯中,它不受編譯器控制,也不由C++定義。具有強定義所有權的程序會比所有權分散的程序更高效。
5、在C++中,動態變量是由 new 表達式創建,由 delete 表達式釋放的。它們會調用C++標准庫的內存管理函數。
6、智能指針會通過耦合動態變量的生命周期與擁有該變量的智能指針的生命周期,來實現動態變量所有權的自動化。C++允許多個指針和引用指向同一個動態變量,共享了所有權的動態變量開銷更大。
7、靜態的創建類成員並且在有必要時采用“兩段初始化”,這樣可以節省為這些成員變量分配內存的開銷。
8、讓主指針來擁有動態變量,使用無主指針替代共享所有權。
9、從性能優化的角度上看,使用指針或是引用進行賦值和參數傳遞,或是返回指針或引用更加高效,因為指針和引用時存儲在寄存器中的。
10、當一個數據結構中的元素被存儲在連續的存儲空間中時,我們稱這個數據結構為扁平的,相比於通用指針鏈接在一起的數據結構,扁平數據結構具有顯著的性能優勢。
六、優化熱點語句
1、除非有一些因素放大了語句的性能開銷,否則不值得進行語句級別的性能優化,因為所能帶來的性能提升不大。
2、循環中的語句的性能開銷被放大的倍數是循環的次數。函數中的語句的性能開銷被放大的倍數是函數被調用的次數。被頻繁地調用的編程慣用法的性能開銷被放大的倍數是其被調用的次數。
3、從循環中移除不變性代碼(當代碼不依賴於循環的歸納變量時,它就具有循環不變性),不過現代編譯器非常善於找出循環中被重復計算的具有循環不變性的代碼。
4、從循環中移除無謂的函數調用,一次函數調用可能會執行大量指令,這是影響程序性能的一個重要因素,如果一個函數具有循環不變性,那么將它移除到循環外有助於改善性能。有一種函數永遠都可以被移動到循環外部,那就是返回值只依賴於函數參數而且沒有副作用的純函數。
5、從循環中移除隱含的函數調用;如果將函數簽名從通過值傳遞實參修改為傳遞指向類的引用和指針,有時候可以在進行隱式函數調用時移除形參構建。
6、調用函數的開銷是非常小的,只是執行函數體的開銷可能非常大,如果一個函數被重復調用多次則累積的開銷會變得很大。函數調用的開銷主要包括函數調用的基本開銷、虛函數的開銷、繼承中的成員函數調用、函數指針的開銷等。函數的調用開銷雖然很大,但正因為函數調用才實現了程序的一些復雜的功能。
6、調用操作系統的函數的開銷是高成本的。
7、內聯函數是一種有效的移除函數調用開銷的方法。
七、使用更好的庫
1、C++為常用功能提供了一個簡潔的標准庫。
*確定哪些依賴於實現的行為,如每種數據類型的最大值和最小值。
*易於使用但是編寫和驗證都很繁瑣的可移植的超越函數(超越函數指的是變量之間的關系不能用有限次加、減、乘、除、乘方、開方運算表示的函數),如正弦函數和余弦函數、對數函數和冪函數、隨機數函數等等。
*除了內存分配外,不依賴於操作系統的可移植的通用數據結構、如字符串、鏈表和表。
*可移植的通用數據查找算法、數據排序算法和數據轉換算法。
*以一種獨立於操作系統的方式與操作系統的基礎服務相聯系的執行內存分配、操作線程、管理和維護時間以及流I/O等任務的函數。
2、使用C++標准庫的注意事項
*標准庫的實現中有bug,(標准庫和編譯器是單獨維護的,編譯器中也可能存在bug,標准需求的改變、責任的分散、計划問題以及標准庫的復雜度都會不可避免地影響它們的質量)。
*標准庫的實現可能不符合C++標准,(庫的發布計划和編譯器是不同的,而編譯器的發布計划與與C++標准不同,一個標准庫的實現可能會領先或是落后於編譯器)。
*對於標准庫開發人員來說,性能並非是最終要的事情,(因為庫會被長期使用,所以庫的簡單性和可維護性更加重要)。
*庫的實現可能會讓一些優化手段失效,C++標准庫中的有些部分並非是有用的。
*標准庫不如最好的原生函數,(標准庫沒有為某些操作系統提供異步文件I/O等特性,性能優化人員只能通過調用原生函數,犧牲可移植性來換取運行速度)。
3、C++標准庫之所以提供這些函數和類,是因為要么無法以其他方式提供這些函數和類,要么這些函數和類被廣泛地用於多種操作系統上。在對庫進行性能優化時,測試用例非常關鍵;接口的穩定性是可交付的庫的核心。
4、扁平繼承層次關系(多數抽象都不會有超高三層類繼承層次,一旦超高三次可能表明類的層次結構不夠清晰,其引入的復雜性會導致性能的下降)。
扁平調用鏈(絕大多數抽象的實現都不會超高三層嵌套函數的調用,在已經充分解耦的庫中是不會包含冗長的嵌套抽象調用鏈的)。
八、優化算法
1、高效的算法是計算機科學一直研究的主題,計算機科學家十分重視算法和數據結構的研究,因為它是展示優化代碼的典型事例。當一個程序需要數秒內執行完畢,實際上卻要花費數小時時,唯一可以用成功的優化方法可能就是選擇一種高效的算法了。算法是一個非常重要且不能簡而概之的主題,可以參考《算法導論》,進行更深入的學習。
2、優化模式
開發人員研究算法和數據結構的原因之一是其中蘊含着用於改善性能的“思維庫”,這些改善性能的通用技巧是非常的使用的,其中的一些模式也是數據結構、C++語言特性和硬件創新的核心。
* 預計算;可以在程序早期,通過在熱點代碼前執行執行計算來將計算從熱點部分中移除。
* 延遲計算;通過在正真需要執行計算時才執行計算,可將計算從某些代碼路徑上移除。
* 批量處理;每次對多個元素一起進行計算,而不是一次只對一個元素進行計算。
* 緩存;通過保存和復用高代價計算的結果來減少計算量,而不是重復進行計算。
* 特化;通過移除未使用的共性來減少計算量。
* 提高處理量;通過一次處理一大組數據來減少循環處理的開銷。
* 提示;通過在代碼中加入可能會改善性能的提示來減少計算量。
* 優化期待路徑;以期待頻率從高到低的順序對輸入數據或是運行時發生的事件進行測試。
* 散列法;計算可變成字符串等大型數據結構的壓縮數值映射(散列值)。在進行比較時,用散列代替數據結構可以提高性能。
* 雙重檢查;通過先進行一項開銷不大的檢查,然后只在必要時才進行另外一項開銷昂貴的檢查來減少計算量。
九、優化查找和排序
1、改善查找性能的工具箱,測量當前的實現方式的性能來得到比較基准,識別出待優化的抽象活動,將待優化的活動分解為組件算法和數據結構,修改或是替換那些可能並非最優的算法和數據結構,然后進行性能測試以確定修改是否有效果。
2、標准庫查找算法接受兩個迭代器參數:一個指向待查找序列的開始位置,另一個則指向待查找序列的末尾位置(最后一個元素的下一個位置)。所有的算法還都接受一個要查找的鍵作為參數以及一個可選的比較函數參數。
3、使用C++標准庫優化排序,在能夠使用分而治之算法高效地進行查找之前,我們必須先對序列容器排序,C++標准庫提供了兩種能夠高效地對序列容器進行排序的標准算法——std::sort()和std::stable_sort()。
十、優化並發
1、並發是多線程控制的同步執行,並發的目標不是減少指令執行的次數或是每秒訪問數據的次數,而是通過提高計算資源的使用率來減少程序運行的時間的。
2、有很多機制能夠為程序提供並發,其中有些基於操作系統或是硬件。C++標准庫直接支持線程共享內存的並發模型。
3、計算機硬件、操作系統、函數庫以及C++自身的特性都能夠為程序提供並發支持。
* 時間分隔;這是操作系統的一個調度函數,為每個程序都分配時間塊。操作系統是依賴於處理器和硬件的。它會使用計時器和周期性的中斷來調整處理器的調度。
* 虛擬化;虛擬化技術是讓操作系統將處理器的時間塊分配給客戶虛擬機,計算資源能夠根據每台客戶虛擬機上正在運行的程序的需求進行分配。
* 容器化;容器中包含了程序在檢查點的文件系統鏡像和內存鏡像,其主機是一個操作系統,能夠直接提供I/O和系統資源。
* 對稱式多處理;是一種包含若干執行相同機器代碼並訪問相同物理內存的執行單元的計算機,現代多核處理器都是對稱式多處理器。使用正真的硬件並發執行多線程控制。
* 同步多線程;有些處理器的硬件核心有兩個或多個寄存器集,可以相應地執行兩條或多條指令流。最高效第使用軟件線程的方法是讓軟件線程數量與硬件線程數量匹配。
* 多進程;進程是並發的執行流,這些執行流有它們自己的受保護的虛擬內存空間,進程之間通過管道、隊列、網路I/O或是其他不共享的機制進行通信,進程的主要優點是操作系統會隔離各個進程,使其不會互相干擾影響。
* 分布式處理;是指程序活動分布在一組處理器上,這些處理器可以不同。分布式處理系統通常會被分解為子系統,形成模塊化的,易於理解的和能夠重新配置的體系結構。
* 線程;線程是進程中的並發執行流,它們之間共享內存;與進程相比,線程的優點在於消耗的資源更少、創建和切換也更快。由於進程中的所有線程都共享相同的內存空間,所以一個線程寫入無效的內存地址可能會覆蓋掉其他線程的數據結構,導致線程奔潰或是出現不可預測的情況。
* 任務;任務是一個獨立線程的上下文中能夠被異步調用的執行單元,任務運行的基礎是線性池。基於任務的並發構建於線程之上,因此任務也具有線程的優點和缺點。
4、如果沒有競爭,那么一個多線程C++程序具有順序一致性,理想的競爭一塊短臨界區的核心數量是兩個。在臨界區中執行I/O操作無法優化性能,可運行線程的數量應當少於或等於處理器核心數量。
后記
以上是我關於C++性能優化指南的筆記,主要針對我個人的知識盲點、核心概念要點的簡單記錄;如有錯誤,希望大家批評指正!