線上課|《C++性能優(yōu)化高端培訓(xùn)》6月正式開講

計算機系統(tǒng)軟件方面的性能優(yōu)化一直是企業(yè)在技術(shù)攻關(guān)過程中需要不斷突破的難題,也是 Boolan 技術(shù)賦能培訓(xùn)的重點內(nèi)容之一。為此,我們特別將線下企業(yè)內(nèi)訓(xùn)課程搬到線上,讓更多人能有機會學(xué)習(xí)并掌握性能優(yōu)化的技巧。
《C++性能優(yōu)化高端培訓(xùn)》在線直播精品課程即將于6月22日重磅上線,由 Boolan首席咨詢師、性能優(yōu)化專家吳詠煒老師主講。
講師介紹

5月31日晚,吳詠煒老師現(xiàn)身 Boolan 直播間,和大家一起聊了聊《現(xiàn)代C++性能漫談 》,以下為本期直播重點干貨:
講座回顧
一、影響性能的架構(gòu)因素

講優(yōu)化離不開大O表示法,大O表示法雖然是算法里面非常重要的一個東西,但在我們真實的項目中是遠遠不夠的,還有很多其他影響性能的因素,有硬件方面的,也有軟件方面的。硬件方面關(guān)系比較大的是存儲層次體系,還有處理器的亂序執(zhí)行和流水線的問題,還有一些是并發(fā)的問題。軟件方面常見的是系統(tǒng)調(diào)用的開銷問題、編譯器優(yōu)化問題和語言本身的抽象性問題。
1. 硬件——存儲層次體系

這張圖講的是計算機的存儲架構(gòu)問題。這個金字塔的頂端,標準是處理器里面的寄存器,它是最快、最貴的,同時也是最小的,物理尺寸和容量都非常小。往下有處理器的緩存,再往下有隨機存儲,再往下有閃存硬盤,到現(xiàn)在已經(jīng)不太常見的磁帶備份,我們可以看到越往下,它就越來越大,容量越來越大,速度越來越慢,價格越來越便宜,這是一個趨勢,我們能不能完全使用非常快、非常貴的那種東西來做出一個大容量的計算機呢?當然理論上是可能的,但從經(jīng)濟學(xué)角度來講性價比非常低。因為你并不一定所有時間都能利用得上這些快速存儲的東西,我們很多時候可以分層,有一部分是快的,有一部分是慢的,只要能保證比較快地把這些慢的東西加載到快的里面去執(zhí)行,這樣我們價格可以做得非常低,同時性能仍然差不多,而并不是把處理器里面最快的那些東西做到容量跟下面一樣大,讓這個電腦的價格貴上幾百萬倍,性能能提升幾百萬倍。

以常見的 Intel Haswell 處理器為例,可以看到從上往下基本是以量級性的大小在增加,延遲也是一樣量級性地增大,所以整體來看會發(fā)現(xiàn)存儲是一定有這樣一個層次的。存儲層次實際上是我們軟件優(yōu)化當中一個非常需要注重的問題:怎么高效地利用存儲,在很大程度上決定了你的代碼性能。
講性能實際上要考慮好幾個因素,有一個要考慮的點就是你的程序到底瓶頸在哪里。你程序的瓶頸可能是CPU,也可能是內(nèi)存,也可能是 IO。優(yōu)化的處理策略也會不一樣,比如,如果你的應(yīng)用瓶頸在內(nèi)存上的話,那你需要特別關(guān)心存儲層次這個問題。

這張圖看起來更直觀一些,可以看到主存和 L1 差不多有50倍的性能差異。如果要訪問的數(shù)據(jù)在 L1 里,那就比從主存里直接取要快50倍,如果已經(jīng)在 L3 里了, 那也比主存取大概要快5-6倍。換句話說,如果你的程序大部分數(shù)據(jù)能夠在 L3 緩存里存得下的話,比每次都要從主存里去取,這個性能能夠提高6倍左右。

存儲訪問的基本原則就是局域性,連續(xù)、不跳躍的存儲訪問最快。這個包含代碼,也包含數(shù)據(jù)。從代碼的角度來講也就意味著順序執(zhí)行,沒有任何跳轉(zhuǎn),沒有任何條件,這樣的程序是最快的;從數(shù)據(jù)的角度來講,連續(xù)存放的數(shù)據(jù)是最快的。如果你訪問數(shù)據(jù)的時候不需要在內(nèi)存里跳來跳去,這樣就會比較快。當然內(nèi)存本身實際上是沒有這個特性的,但考慮到緩存機制,就會有這個問題,硬盤更是如此。硬盤即使不考慮緩存,都是連續(xù)訪問,會比跳躍式的方面要快。
2. 硬件——處理器的亂序執(zhí)行和流水線

處理器里也有所謂的亂序執(zhí)行和流水線,這兩個特性主要都是為了提高在單位時鐘周期的處理能力。后面放的一張圖就會看到為什么我們有很大的需求需要提高單位時鐘周期的處理能力,它對我們程序的影響是程序一旦有分支就會打亂流水線,性能下降,也就是說程序里分支越少越好,條件語句越少越好,甚至調(diào)函數(shù)也是越少越好。

這張圖表現(xiàn)摩爾定律的局限性,主要是說明想要利用摩爾定律帶來的性能提升的話,我們已經(jīng)不能利用主頻的提高了,哪怕是單位時鐘周期的執(zhí)行的指令數(shù)提高也是非常受限的,所以我們需要主動去利用多核的并發(fā)并行方面的特性,這個是很難的。
3. 硬件——并發(fā)對編程思維帶來沖擊:
不再能假設(shè)有自然的完全執(zhí)行順序
開發(fā)人員也必須主動利用多核特性
多線程的調(diào)度和競爭成為影響性能的關(guān)鍵因素
適用于單線程的接口可能不再適用
4. 軟件——系統(tǒng)調(diào)用開銷:

除了極少數(shù)非常硬核的嵌入式開發(fā)可能會完全不需要調(diào)用操作系統(tǒng),一般情況下我們總是需要調(diào)用操作系統(tǒng)的接口,在這種情況下就需要考慮操作系統(tǒng)調(diào)用本身的開銷。圖中列了幾個系統(tǒng)調(diào)用,大家知道它們哪個快哪個慢嗎?需要經(jīng)過測試才能知道哪些比較快。我只知道其中 gettimeofday 是比較快的,在比較新的 Linux 上會是如此,因為 Linux 對 gettimeofday 有個特殊的優(yōu)化,能夠在系統(tǒng)調(diào)用時不進行用戶態(tài)和核心態(tài)的切換。而大部分系統(tǒng)調(diào)用都要執(zhí)行用戶態(tài)和核心態(tài)之間的切換,就會有一個額外的開銷,性能會低很多。
5. 軟件——編譯器優(yōu)化:

編譯器本身也是 C++ 里的一個大魔法,編譯器的優(yōu)化可以產(chǎn)生非常巨大的性能差異,它對 C++ 帶來的性能差異的影響比 C 帶來的性能影響要大得多,很大程度上是因為程序怎么寫造成的。優(yōu)化本身一方面能提高性能,另一方面會揭露你程序里的短處,因為優(yōu)化的時候,你覺得程序這么寫沒問題,可能在沒有優(yōu)化的時候也確確實實沒有問題,但是一旦打開了優(yōu)化,你會發(fā)現(xiàn)程序里突然出了“鬼”,就是因為你的代碼里可能寫出了某些未定義行為。未定義行為在 C++ 里非常妖孽,但它對優(yōu)化來講也非常重要。比如空指針的解引用就是一個比較常見的未定義行為,當然在大部分現(xiàn)代環(huán)境里都有保護模式,一旦有非法內(nèi)存訪問,包括空指針,在用戶態(tài)執(zhí)行的程序會立即掛掉。程序掛掉實際上是件好事情,讓你立即能夠進行調(diào)試,知道問題出在哪里。但有很多未定義行為編譯器沒法發(fā)現(xiàn)、沒法告警,然后在執(zhí)行過程中你才會突然發(fā)現(xiàn)程序行為不正常了,你調(diào)了半天才發(fā)現(xiàn),哦,原來我這里觸發(fā)了一個未定義行為。
打個比方,帶符號整數(shù)的溢出在 C++ 里就是一個未定義行為,你如果認為兩個正整數(shù)相加有可能會變成負數(shù),會翻轉(zhuǎn)的話,那就錯了。C++ 編譯器有可能會認為這種情況不可能發(fā)生,這種情況下,代碼的行為就可能跟你想象的行為會不一樣。編譯器會有一些告警選項,你還可以使用一些像 clang-tidy 這樣的靜態(tài)掃描工具,部分可以解決這些問題,但這也只是部分。你仍然需要了解這些。如果你不知道這些未定義行為,如果你沒有打開告警選項,如果你也沒利用靜態(tài)掃描器,那很可能這些未定義行為會把你坑得很慘。

這是另外一個特殊的編譯器的行為,編譯器對硬件特性的照顧。它產(chǎn)生的代碼會跟你想象的有一點點不一樣。這邊有x、y、a三個全局變量,看代碼,比較直觀的想象應(yīng)該是,先寫x、再寫y。但是我們看一下右邊圖,會發(fā)現(xiàn)生成的匯編代碼里,它先讀a,再寫y,再寫x,這就是編譯器為了照顧處理器進行了一個亂序處理?,F(xiàn)代處理器里為了在單個周期能盡量多執(zhí)行指令,內(nèi)部有流水線,可以并行執(zhí)行多條指令。把讀a和把結(jié)果寫到x里面,這兩個操作有依賴關(guān)系,不能并行。所以編譯器會把寫y穿插進去,因為這條指令跟讀a是不沖突的,可以并行執(zhí)行。我們可以看到,你即使沒寫任何特殊的東西,編譯器都可能會做一些亂序,而處理器本身也還可能會產(chǎn)生額外的亂序。
6. 軟件——語言抽象性:

語言的抽象性,這個可能就比較好理解了,和比較直白的 C 相比起來,在 C++ 里如果寫同樣的 Obj obj,除了和 C 相同的步驟之外,還會調(diào) Obj 的構(gòu)造函數(shù),當代碼在執(zhí)行到下面的 } ,會調(diào)用析構(gòu)函數(shù),這就是一個比較隱式的一個操作。
二、為什么要用 C++?

為什么我們要使用 C++?首先我們要貼近硬件,需要使用一些原生指令,需要高性能,需要使用一些新的特殊硬件;另一方面我們又需要使用零開銷抽象。只要求貼近硬件的話,用 C 就可以,而 C++ 額外提供的就是多種不同的零開銷的抽象機制,包括類、包括繼承、模板等等好多東西,都是為了讓你能夠進行抽象,同時你這個抽象本身的開銷應(yīng)該是你想象中最低或者比較低的,跟你手寫出來代碼一樣,而且在你不使用這個抽象的時候不會給你帶來額外的開銷。這就是 C++ 的重要特性。

C++ 的學(xué)法,我認為正確的做法是把它當成一門外語,而且是一門活躍的外語,像英語這樣的外語,而不是像拉丁語。你需要持之以恒,你需要練習(xí),你需要掌握慣用法,你還要學(xué)習(xí)一些最佳實踐。

C++ 里提供了很多抽象,所以你要利用這些抽象,我們可以寫出很抽象和比較簡潔的代碼,這就是洋蔥原則的第一條:簡單的事情簡單做。我們不需要把所有的事情都搞得很復(fù)雜,C++ 的特點在于如果你需要的時候,C++ 給你有很多定制點,可以允許你去做很多額外的切割,你可以做些額外的解剖,當然解剖得越深,你會哭得越多,因為你會碰到頭痛的問題越多,但是通過這些機制 你可以做很多進一步的優(yōu)化,但我們原則仍然是首先把代碼寫正確,然后進行優(yōu)化。有句老話是:過早優(yōu)化是萬惡之源。
我們既然用 C++ 這樣的語言,肯定是想要做優(yōu)化的,我們一定會要性能,我們一定會要優(yōu)化,但我們不可能在所有的地方都寫出最優(yōu)的代碼,這個開銷有點大, 就像前面洋蔥原則提到的那樣,我們可能會哭死。
三、性能優(yōu)化

阿姆達爾定律是說我們能夠?qū)Υa提升性能的提高程度是取決于你的代碼在整個程序里占了多大的比例。P代表的是你所優(yōu)化的部分在你整個應(yīng)用里的開銷的比例。我們想提高執(zhí)行性能的話,就是執(zhí)行時間的百分比。Sp 是你提高的部分,打個比方,我如果有一部分的代碼在整個應(yīng)用里面占了50%的開銷,那p就是0.5,然后如果我把這一部分的性能整整提高了50%,那Sp就是1.5,代入這個公式就會發(fā)現(xiàn)結(jié)果是1.2,我們性能總體提升了20%。想象一下,如果我們把某一部分性能提高了100%,但是這一部分占整個系統(tǒng)運行開銷的1%,那我們現(xiàn)在能提高多少?只有百分之零點幾,哪怕這個東西提高了無數(shù)倍,也不可能超過1%的性能提升。所以我們軟件要做優(yōu)化的話,一定要找出瓶頸,來優(yōu)化這個瓶頸。
所以下面我們要討論的一個問題就是性能的測試,性能的測試有一個測不準問題,我們這邊稍微展開一下:

大家可以稍微看一眼這個小例子,我們想測試的是memset的性能,左邊是memset,右邊是一個簡單的 for 循環(huán)來進行清理,我們循環(huán)若干次,想看最后這兩個clock減一下,得出我們輸出執(zhí)行時間的差異。

可以看到在O0的情況下,memset比手工循環(huán)要快出50倍以上,memset 似乎比較快,O1 這個比值就減少了,O1、O0零是不是更接近真實呢?再看O2,會發(fā)現(xiàn)出了一個很奇怪的鬼,memset比手工循環(huán)慢了10萬倍,第一次碰到這個問題肯定就是滿頭霧水了,原因很簡單:對 buffer 的寫入被優(yōu)化掉了。如果你出現(xiàn)了memset 比手工循環(huán)慢,那可能的原因就是 memset 沒有被優(yōu)化掉,把手工循環(huán)完全被優(yōu)化沒了。我們有時候就會想到 volatile 能不能幫助解決這個問題。

這是個執(zhí)行結(jié)果,我們可以看一下,看起來似乎正常一點,加入 volatile 之后我們會發(fā)現(xiàn) O1 和 O2的結(jié)果一致了,都是memset大概是手工循環(huán)的性能的5倍,這是不是最后的結(jié)果呢?還不是。因為 volatile 本身會妨礙優(yōu)化。
我們可以看一下,同樣是手工循環(huán)寫入,一個是有 volatile,一個是沒有 volatile,然后我們看一下匯編,對于有 volatile 的我們發(fā)現(xiàn)匯編里面編譯器產(chǎn)生的這個代碼就是很老實的,一次寫一個字節(jié),一共循環(huán)80次,寫入到內(nèi)存里面去,就這么簡單,右邊這個代碼就不一樣了,我們會發(fā)現(xiàn)編譯器連循環(huán)都沒有了,直接一次寫16個字節(jié),連續(xù)展開寫五次結(jié)束,很明顯右邊這個代碼的執(zhí)行性能肯定比左邊的高,這是可以想象的一件事。
所以我們測試的角度來講就一定要防優(yōu)化,有時候我們要讓編譯器不要做一些不必要的優(yōu)化,volatile 是得謹慎使用,它確實可以防止優(yōu)化,但有時會防止得過頭,而且它雖然能夠防止編譯器的重排序,但防止不了處理器的重排序。使用全局變量肯定是有好處的,你往全局變量里寫一定會寫入,如果往本地變量里寫,那編譯器有可能就把這個東西完全優(yōu)化。還有用鎖可以當簡單的內(nèi)存屏障,這是比較可靠的 C++ 內(nèi)存模型,能保證在這種情況下性能是令人滿意的,但用鎖的話性能開銷就會比較大,時間會比較大,我們還可以使用 noinline 來防止一般的內(nèi)聯(lián)。但不管怎么樣,我們可以發(fā)現(xiàn)如果把這些東西組合在一起,我們測試精度就需要提高,用clock 實際上是有點問題的,特別在Windows下它的精度太低了,應(yīng)該只有1毫秒的精度。
在 Linux 下 clock 的精度稍微高一點,能達到1微秒,但我測過一個結(jié)果,它的耗時比較大。實際上我們可以看一下這里面,最優(yōu)選的,在 Intel 平臺上應(yīng)該是個叫 rdtsc 的一條指令,它一般有內(nèi)聯(lián)匯編,可以直接使用 rdtsc 這個函數(shù),精度達到了 10 納秒,比其他的都要高,開銷也要低一點。如果你沒有 rdtsc,就可以考慮系統(tǒng)里面提供的 system_clock,steady_clock,high_resolution_clock 三個 C++ 里帶來的時鐘。我一般會推薦 steady_clock,因為它能夠保證時鐘是穩(wěn)定的,保證你測試的過程中不會因為有某個進程進行系統(tǒng)的對時之類的操作,發(fā)生時間反轉(zhuǎn)之類的情況。利用這些東西,我們實際上就可以測出比較高精度的結(jié)果。
這是我利用前面說的加鎖、rdtsc 等等去測函數(shù)調(diào)用和虛函數(shù)調(diào)用的額外開銷的差異問題,我測出來的一個結(jié)果:每次函數(shù)調(diào)用的開銷本身大概是2.5個時鐘周期,虛函數(shù)調(diào)用的開銷大概是4個時鐘周期,這兩個開銷本身的差異不大。不過我們后面會提到,雖然這看起來差異不大,但實際上仍然有問題,而且我這個測試實際上是有一點點問題的,因為在測試當中很有可能你調(diào)用的函數(shù)會變熱, 也就是說多次調(diào)用了,就會在緩存里,你每次執(zhí)行就會性能比較高,但實際情況下有可能不在緩存里,這樣的話性能差異就又會變大了。但也有很多其他因素,我們后面有個例子可以具體再看一下。
性能測試有兩種方法,前面說的就屬于是右邊這一種“插樁測試”,這實際上不是最優(yōu)先的測試方法,它適用于已經(jīng)明確知道瓶頸在某個函數(shù)的情況下,就盯著這個函數(shù)去測,看看怎么樣能夠提高這個函數(shù)的性能。一般來講,你在找瓶頸的時候會使用左邊這種“采樣測試”,采樣測試一般需要編譯器或者是操作系統(tǒng)來提供一些支持。GCC 本身有個 gperftools,但用起來并不是特別方便,一般我推薦是 Google 的 perftools 會好一點。Linux 本身也有個 perf,這些都是比較好的采樣測試的方式,一般來講比較推薦用采樣測試來找出性能瓶頸點,然后在后續(xù)的測試當中可以考慮用插樁測試來把你想要提高性能的那個函數(shù)的性能精確地測一下,把它性能提高。
總體來講,我們要考慮90/10的規(guī)律,也就是說我們優(yōu)化的一定是在瓶頸上的一部分代碼,剩下的部分就可以不用去優(yōu)化,或者說等前面的優(yōu)化完了再去優(yōu)化,因為我們需要考慮生產(chǎn)率和性能的權(quán)衡問題。如果你把時間大量耗費在沒有必要的優(yōu)化上,那你總體生產(chǎn)率就太低了,有可能這個程序的性能反而提不上去。所以再強調(diào)一遍:過早優(yōu)化是萬惡之源。
視圖類型是說某一個對象不擁有指向的資源,一般來講它只是指針加長度這樣輕量的東西。但它用起來比較方便,我們利用 C++ 里面的一些構(gòu)造和隱式轉(zhuǎn)換可以很方便地使用視圖,同時可以對接口進行同一化處理。只要你保證底層的數(shù)據(jù)一直存在,那我們實際就可以使用指針加長度這樣一個比較輕量級的對象來訪問。這樣一個視圖類型對象的復(fù)制開銷為 O1,所以做傳參的時候拷貝這個或者新構(gòu)造這樣一個對象都比較方便。我們常見的視圖類型有 string_view,這是 C++ 17 里有的;然后 C++ 20里面會有span,或者你等不及 C++ 20 的話,只要你使用 C++ 14 以上的版本,可以用 gsl::span。C++ 20 在 ranges 下面還有好多的視圖,這些視圖用起來都非常方便,而且性能還非常高。這就是C++ 提供d的一種抽象方式,來幫你解決一些性能問題。
優(yōu)化選項也是另外一個和性能關(guān)系很大的東西,像 GCC 下面有一大堆的優(yōu)化選項,細分的話大概一共有100多個,這里就不一一例舉了。我們看一個很具體的例子。
這個例子里面,我有3個測試,一組測試是 sort 加上一個函數(shù)對象,我用的是 less;然后是 sort 加上一個普通的函數(shù),就是你寫一個函數(shù)來比較兩個東西的大小,我利用這個函數(shù)指針去傳給sort 來進行排序;還有一個就是 C 里面就有的 qsort,也是利用一個普通函數(shù)來進行排序。我們會發(fā)現(xiàn)在 O0 的情況下,C 性能會比 C++ 的要高,但一旦你開到了 O2,C++ 代碼的性能就反過來比 C 的要高出很多了。而且我們可以看一下 O2 對 O0 的性能提升,在 sort 加函數(shù)對象的情況下是14倍的性能提升,而對于 qsort 是 1.57倍,換句話說,在 C 的年代,你可能可以承受得了不開O2、O3,但在 C++ 的年代里恐怕有點難,因為你不開這些優(yōu)化選項的話,你這個代碼可能會發(fā)現(xiàn)性能很低,因為標準庫里的很多東西,如果你不開優(yōu)化,特別是沒有 inline 的話,是絕對不可以的,因為 inline 不 inline,本身就可以帶來一個數(shù)量級的性能差異。
我們看一下這張圖,上邊左邊是sort_with_func,中間是 sort_with_less,這兩個就是前面說的使用函數(shù)指針和使用函數(shù)對象。我們可以發(fā)現(xiàn)這個函數(shù)調(diào)用非常復(fù)雜,就是因為 sort 函數(shù)里實際上是有很多層的函數(shù)調(diào)用。C++ 里有很多小函數(shù),每一個的圈復(fù)雜度都比較低,而 C 的話,大家會發(fā)現(xiàn)它的函數(shù)調(diào)用層次不深,就這么幾個函數(shù),所以它能夠容忍你沒有inline、沒有優(yōu)化,都還可以。但是 C++ 里就不行。C++里你要達到比較好的性能,一定要開高優(yōu)化,一定要打開inline。
Q&A
Q. 函數(shù)對象為什么比純函數(shù)快?
A. 因為函數(shù)對象比純函數(shù)更容易被內(nèi)聯(lián),這是最主要的優(yōu)化點。
Q. 采樣測試需要權(quán)限嗎?
A. 采樣測試用perf一般需要root權(quán)限。如果沒有root權(quán)限,可以考慮使用 Google Perftools。
Q. 虛函數(shù)和模板?
A. 模板是靜態(tài)多態(tài),虛函數(shù)是動態(tài)多態(tài)。一般來講,模板能夠做到更高的性能。
Q. 編程語言的抽象性影響性能是什么意思?
A. 是指你寫下一行語句,有可能不知道背后發(fā)生了些什么事情,你需要理解這個語句背后發(fā)生了什么事情,你才知道這個東西對性能有什么關(guān)系。
Q. 工程上很多的性能瓶頸來自于加鎖、IO 等,那對于 C++ 語言本身的一些優(yōu)化是否一般來說并不是性能瓶頸?
A. 我覺得不會這么說,因為首先你需要理解什么情況下該使用加鎖,什么情況下不需要使用加鎖。其次,當解決了一些其他的瓶頸之后,語言本身就可能會成為瓶頸。我們肯定不希望瓶頸一直是在加鎖或 IO上,一定是要想到辦法把這些瓶頸擼掉,而且在這個過程中你也需要利用到 C++ 的一些特性。
Q. Lambda 和函數(shù)對象的關(guān)系?
A. Lambda就是函數(shù)對象,所以我前面提到函數(shù)對象的東西跟Lambda 是共通的,Lambda 就是一個匿名函數(shù)對象,自動幫你生成了一個函數(shù)對象。
Q. bind 性能為什么低?
A. bind產(chǎn)生了一個對象,里面做了類型擦除,調(diào)用它時需要做一次類似于虛函數(shù)這樣的轉(zhuǎn)發(fā)。肯定比不上 lambda,因為如果你用 lambda 的話很大程度上可以做內(nèi)聯(lián),bind 很多情況下不能內(nèi)聯(lián)。一般來講,現(xiàn)代 C++ 已經(jīng)基本上沒有什么需要用 bind 的地方了,都可以用 lambda 代替。
Q.如果用 pool allocator 1000大小的map是不是也可以一次性malloc?
A. 不會,只是說你向系統(tǒng)申請內(nèi)存的次數(shù)少,但 map 本身還是會執(zhí)行 1000 次的分配,最關(guān)鍵在于你內(nèi)存的局域性不一定能保證。當然如果你的 pool allocator 做得好的話確實可以優(yōu)化到接近于我前面說的 vector 這樣的程度。事實上這兩種都不一定是最優(yōu)化的情況;取決于你具體的使用場景,可能可以進一步優(yōu)化。比如說我在實際項目里有一種優(yōu)化的場景是 map 基本上是不變的,這種情況下我可以進一步優(yōu)化到運行時沒有初始化開銷,完全靜態(tài),這種情況下運行時只是做一個查詢,而沒有任何分配開銷了。
Q. vector 1000次的測試數(shù)據(jù)是不是跟 CPU 有關(guān)?
A. 可以這么說吧,具體肯定需要自己測的。我這個數(shù)據(jù)我相信是具有一定普遍性的,但肯定也有平臺相關(guān)性,跟 CPU 可能有一定的相關(guān)性。跟 OS 的相關(guān)性只有一點,就是你使用的內(nèi)存分配器本身的開銷怎么樣 。
Q. nop 用到自旋鎖,低功耗?
A. 我現(xiàn)在是覺得自旋鎖并不一定是個好主意,因為我自己設(shè)計過一些無鎖的數(shù)據(jù)結(jié)構(gòu),實測下來,現(xiàn)在無鎖的數(shù)據(jù)結(jié)構(gòu)不一定有性能優(yōu)勢,性能經(jīng)常還不如直接加鎖,因為無鎖的情況下意味著沒有滿足條件的話就會繼續(xù)往下執(zhí)行,你會繼續(xù)占住CPU,你即使 nop 也是占住 CPU 了,因為你是無鎖的話,做一個spinlock 肯定會有一個循環(huán),會不停在那轉(zhuǎn)圈,不管怎么樣,你會額外做一些操作。如果是加鎖的,當前這個線程就沒有任何操作,完全交給操作系統(tǒng)、交給我的運行庫去調(diào)度。
Q. 什么系統(tǒng)才能用到性能優(yōu)化?
A. 我覺得任何系統(tǒng)都用得到性能優(yōu)化——如果你不需要性能優(yōu)化的話,你干嗎用 C++?用 C++ 就是為了性能。
Q. Lambda一定要用模板參數(shù)接收嗎?
A. 當然不一定了,但是你用模板參數(shù)接收的話,有望達到最高性能。只有在用模板參數(shù)接收這種情況下才能滿足我前面說的內(nèi)聯(lián),如果你用function或者函數(shù)指針的話,都會降低內(nèi)聯(lián)的機會。
Q. 什么情況下使用類型擦除?
A. 每一個函數(shù)對象,每個 lamda 都是不同的類型,所以需要用模板參數(shù)的方式來接收,這種情況下我們能達到最高的性能。但同時因為它必須用模板參數(shù)來接收,所以每一次用不同的對象類型去調(diào)這個函數(shù)模板,都會產(chǎn)生不同的代碼,也就意味著你有可能會有二進制膨脹。這是一種情況。另外一種情況是,你需要用一個 map 或者是 set 或者 vector 來接收一堆不同的函數(shù)對象或者 lambda 表達式,這種情況下,因為你需要讓這個容器能夠放下不同的函數(shù)對象,也需要用類型擦除,也就是說讓不同的對象變成同一種類型的對象,把它變成同一種類型,我只關(guān)心它的參數(shù)、返回值等等。
Q. MSVC的優(yōu)化技巧或工具
A. profiling 用 Visual Studio 本身就自帶的,C++ 語言方面實際上是差不多的,這個其實跟平臺相關(guān)性很弱,主要是profiling 的工具會不一樣。Windows 下就用 Visual Studio,Linux 下會用 perf 或者是 Google perftools 這樣的工具。
Q. VTune 對于性能分析是不是個好工具或者是否有更好的推薦?
A. 如果你在 Intel 平臺上工作的話,那我相信 VTune 應(yīng)該是最好的工具吧。
Q. STL 的效率夠用嗎?
A. 你打開優(yōu)化的話應(yīng)該是夠用的;如果你不打開優(yōu)化,那肯定不夠用。
Q. 分析 RTTI 和異常有什么好的工具和方法?
A.通過普通的性能測試就可以。RTTI 性能取決于你的繼承樹的深度,繼承層數(shù)不深的話,一般來講性能問題不大。異常確實可能性能開銷比較大。如果在不拋異常的happy path,執(zhí)行性能通常來講是沒什么問題,一般會有一些二進制膨脹的問題,但不應(yīng)該會有執(zhí)行性能的問題。一旦拋了異常,對執(zhí)行的性能影響就非常大,所以一般的指導(dǎo)原則是你拋異常的概率應(yīng)該小于1%,而且不應(yīng)該是用戶可以容易造成的。比如,如果你網(wǎng)絡(luò)報文不合法就拋異常,這基本上不可接受:因為這意味著黑客可以制造一些非法的報文,讓你的系統(tǒng)性能降到原先的百分之一、千分之一,這顯然不可接受。所以即使在使用異常的情況下,也不意味著我們所有的錯誤處理都要用異常。異常要用來處理真正異常的情況。
Q. 編譯選項開O3好還是O2?
A. 通常來講是O3好,當然你要試驗一下,性能問題說到底都需要測試,真正你要精調(diào)的話不是開O2或O3,至少在 GCC 下不是,而是要了解O2、O3分別打開那些選項,然后真的逐個去調(diào)這些選項,發(fā)現(xiàn)哪些選項打開是有好處的,哪些選項關(guān)掉更好一點。
Q. Linux版本標準庫的 malloc 是直接new 的嗎?
A. 目前所有系統(tǒng)我了解到的標準庫自帶的 allocate 都是直接去new的,當然new之后,malloc里會怎么做,那就是你 C 的運行時庫的影響了,你到底是使用 ptmalloc,還是jemalloc,還是Google 的 tcmalloc,都會帶來性能上的不同影響。用 mimalloc 也是個方法,反正這些額外的這些 malloc庫通常來講都會比默認的運行時庫里的內(nèi)存分配要好一點,當然一定還是要測試。
現(xiàn)代C++性能優(yōu)化高端培訓(xùn)
