使用 C++ 中的 final 關鍵字 ,到底能否提升效能?不少開發者認為可以,卻沒能給出數據依據。 為此, 本文作者進行了一次測試,親自驗證這個說法的真實性。
原文連結:https://16bpp.net/blog/post/the-performance-impact-of-cpp-final-keyword/
譯者 | 鄭麗媛
出品 | 程式人生(ID:coder_life)
如果你選擇用 C++ 寫程式碼,一定是有理由的,而這個理由很可能就是效能。
在很多有關 C++ 的文章中,我們經常會看到各種「效能提示和技巧」或「這樣做效率更高」的建議。有時這些建議會給你一個合理的詳細解釋,但更多時候,你會發現沒有任何實際數據支持這些說法。
最近我發現了一個奇怪的東西,那就是 final 關鍵字。說起來有些慚愧,我之前沒怎麽了解過這個關鍵詞。許多部落格文章都說,使用 final 可以提升效能,而且是免費的,只需進行微小改動。
但是,讀完這些文章後,你會發現一個有趣的事實:沒有人給出任何相關數據,基本上就純靠一個「相信我吧,兄弟」。
從我的經驗來看,除非有數據支持,否則任何效能提升的說法就全是空談,甚至就算有了數據也得能夠復現才行——因此,作為一名有著高效能 C++ 計畫的優秀工程師,我真的很想驗證這個說法。
有一個我認為非常適合測試 final 關鍵字的絕佳計畫:PSRayTracing(https://github.com/define-private-public/PSRayTracing)。
簡單介紹一下這個計畫:這是一個用 C++ 實作的光線追蹤器,源自 Peter Shirley 所寫的光線跟蹤系列書籍。它主要用於學術目的,但也結合了我編寫 C++ 時的專業經驗。計畫目標是向讀者展示如何(重新)編寫 C++,使其效能更強、更簡潔、結構更合理,因此在 Shirley 博士原始程式碼的基礎上進行了補充和改進。PSRayTracing 有一個重要特性, 能透過 CMake 切換程式碼的更改,還可以提供其他選項,如隨機種子、多核渲染。
這是如何做到的?
利用構建系統,我在 CMakeLists.txt 中添加了一個額外選項:
option(WITH_FINAL "Use the `final` specifier on derived classes (faster?)" OFF)
# ...
if (WITH_FINAL)
message(STATUS "Using `final` spicifer (faster?)")
target_compile_definitions(PSRayTracing_StaticLibrary PUBLIC USE_FINAL)
else()
message(STATUS "Turned off use of `final` (slower?)")
endif()
然後在 C++ 中,我們可以使用預處理器來建立一個 FINAL 宏:
#ifdef USE_FINAL
#define FINAL final
#else
#define FINAL
#endif
而且,它可以輕松地添加到任何你感興趣的類中:
$ rg FINAL
RandomGenerator.hpp
185: class RandomGenerator FINAL : public _GeneralizedRandomGenerator<std::uniform_real_distribution, rreal, RNG_ENGINE> {
Materials/Lambertian.hpp
8: class Lambertian FINAL : public IMaterial {
...
Materials/SurfaceNormal.hpp
7: class SurfaceNormal FINAL : public IMaterial {
...
PDFs/HittablePDF.hpp
7: class HittablePDF FINAL : public IPDF {
...
Objects/Sphere.hpp
19: class Sphere FINAL : public IHittable {
這樣,我們就可以在程式碼庫中隨時開始和停止對 final 關鍵字的使用了。
當然,你可能會說這個方法太過繁瑣,我也這麽覺得。但不得不說,這很適合用來做對照試驗:將 final 關鍵字套用到程式碼中,並根據實驗需要使用或關閉它。
幾乎每個介面都有 final 關鍵字。在架構中,我們有 IHittable、IMaterial、ITexture 等。在 Peter Shirley 光線跟蹤系列的第二本書中,最後一個場景有很多超過 10K 個虛擬物件:
另外,有些場景的數量並不多(可能只有 10 個):
最初的擔憂
對於 PSRT 來說,在測試可能提高效能的東西時,我首先使用的是預設場景 book2::final。啟用 final 後,控制台報告如下:
$ ./PSRayTracing -n 100 -j 2
Scene: book2::final_scene
...
Render took 58.587 seconds
可隨後又恢復了更改:
$ ./PSRayTracing -n 100 -j 2
Scene: book2::final_scene
...
Render took 57.53 seconds
我有點困惑,用了 final 反而更慢了?又跑了幾次後,我發現效能下降得非常小,那些部落格文章一定是在忽悠我……
不過在完全放棄之前,我想最好還是把驗證測試指令碼拿出來看看。在之前的版本中,這個指令碼主要用於對 PSRayTracing 進行模糊測試,版本庫中已經包含了一套小型的知名測試用例。
這套指令碼最初執行大約需要 20 分鐘,此時情況開始變得有趣:指令碼報告稱,使用 final 時速度稍快,執行時間為 11 分 29 秒;不使用 final 則為 11 分 44 秒。
看似只相差了 2% 的時長,實際上卻很重要——我決定,要進一步調查。
大型測試
由於對以上結果不滿意,我建立了一個「大型測試套件」,主要提高了一些測試參數以加強測試強度。在我的開發機器上,它需要執行 8 個小時。以下是調整後的詳情:
● 場景測試次數:10 → 30
● 影像尺寸:[320x240, 400x400, 852x480] → [720x1280, 720x720, 1280x720]
● 光線深度:[10, 25, 50] → [20, 35, 50]
● 每像素采樣次數:[5, 10, 25] → [25, 50, 75]
我認為這樣更全面:現在有些測試用例只需 10 秒就能完成,有些則需要 10 分鐘才能完成;小型測試套件在 20 多分鐘內完成了約 350 個測試用例,而這個套件在 8 多小時內完成了 1150 多個測試用例。
考慮到 C++ 程式的效能與編譯器(和系統)密切相關,因此為了更加徹底,我們在三台機器、三種作業系統和三種不同的編譯器上都進行了測試;一次使用了 final,一次沒使用。經過計算,這些電腦累計執行了 125 多小時。
具體情況請參見下表,另外配置如下:
● AMD Ryzen 9:
Linux:GCC & Clang
Windows:GCC & MSVC
● Apple M1 Mac:GCC & Clang
● Intel i7:Linux GCC
例如,一種配置是「AMD Ryzen 9,使用 Ubuntu Linux 和 GCC」,另一種是「Apple M1 Mac,使用 macOS 和 Clang」。註意,並非所有編譯器的版本都相同,有些很難獲得。另外在我發文的時候,Clang 還釋出了一個新版本。下面是測試結果的總體摘要:
透過對比測試,我們可以看到一些有趣的結論,同時也告訴了我們一件事:從整體上看,使用 final 不能保證總是提速,甚至某些情況下速度還會更慢。
雖然在這個測試中對編譯器進行比較可能也很有趣,但認為這樣做並不公平:僅把「使用 final」和「不使用 final」進行比較才是公平的。如果想要比較編譯器(以及不同系統),需要更全面的測試系統。
盡管如此,我們還是觀察到了一些有趣的結果:
x86_64 上的 Clang 執行速度較慢。
Windows 效能較差,微軟自己的編譯器也很落後。
蘋果公司的芯片則是絕對的強者。
但每個場景都不同,包含的標記為 final 的物件數量也不盡相同。按百分比來看,有多少測試用例在使用 final 後更快或更慢都很有趣。將這些數據列表,我們可以得到以下結果:
對於某些 C++ 應用程式來說,那 1% 的效能提升非常令人期待(例如,高頻交易)。如果我們 50% 以上的測試用例都能達到這一點,那我們似乎應該考慮使用 final。但另一方面,我們還需要看看相反的情況:例如速度慢了多少?又有多少測試用例變慢了?
在 x86_64 Linux 上的 Clang 就絕對是一個典型:超過 90% 的測試用例在使用 final 後至少慢了 5%!!還記得我說過,對於某些應用程式來說,1% 的速度提升是件天大的好事嗎?所以相對的,就算只慢了 1% 也絕不能容忍。此外,使用 MSVC 的 Windows 也表現不佳。
如上所述,這與場景有很大關系。有些場景只有少量的虛擬物件,有些則有一大堆。下面平均來看,使用 final 後一個場景的速度快了/慢了多少:
我對 Pandas 不是很了解,在建立一個多級索引表格(從陣列中建立)並使其具有良好的樣式和格式方面遇到了一些問題。因此,我在每一列名稱的末尾都附加了一個配置編號。以下是每個數位的含義:
0 - GCC 13.2.0 AMD Ryzen 9 6900HX Ubuntu 23.10
1 - Clang 17.0.2 AMD Ryzen 9 6900HX Ubuntu 23.10
2 - MSVC 17 AMD Ryzen 9 6900HX Windows 11 Home (22631.3085)
3 - GCC 13.2.0 (w64devkit) AMD Ryzen 9 6900HX Windows 11 Home (22631.3085)
4 - Clang 15 M1 macOS 14.3 (23D56)
5 - GCC 13.2.0 (homebrew) M1 macOS 14.3 (23D56)
6 - GCC 12.3.0 i7-10750H Ubuntu 22.04.3
這就是讓人眼前一亮的地方:在某些配置和特定場景下,效能可能會提升 10%!例如,在 AMD 和 Linux 上使用 GCC 的 book1::final_scene。但其他場景(在相同的配置下)僅有 0.5% 的提升,比如 fun::three_spheres。
但是,只是將編譯器切換到 Clang(仍在 AMD 和 Linux 上執行)後,這兩個場景的效能就分別下降了 5% 和 17%!MSVC(在 AMD 上)的情況有點復雜,有些場景在使用 final 時效能更高,有些場景則受到了很大影響。
蘋果的 M1 有點意思,提速和降速振幅看起來都非常小,但 GCC 在兩個場景上有顯著優勢。
另外,使用 final 後效能的提升或降低,幾乎與虛擬物件數量是多是少沒有關系。
我比較關註 Clang 的情況
PSRayTracing 也可在 Android 和 iOS 上執行。在這兩個平台上,可能只有一小部份應用程式為了效能是用 C++ 編寫的,而 Clang 正是用於這兩個平台的編譯器。
不幸的是,我沒有像在桌面系統上那樣的效能測試框架,但我可以做一個簡單的「使用相同參數渲染場景,一個使用 final,一個不使用 final」的測試,因為應用程式會報告行程耗時。
根據上面的數據,我的假設是,這兩個平台在使用 final 後效能會變差,但具體變差多少不清楚。以下是測試結果:
iPhone 12:我認為沒有區別;無論使用 final 與否,渲染相同的場景都需要大約 2 分鐘 36 秒。
Pixel 6 Pro:使用 final 後速度變慢了。渲染時間分別為 49 秒和 46 秒,3 秒的差異可能看起來不是很大,這相當於 6% 的減速,意義相當重大。
我不知道這是 Clang 的問題還是 LLVM 的問題。如果是後者,這可能對其他 LLVM 語言(如 Rust 和 Swift)也有影響。
未來的計劃(以及我希望自己做的事情)
總的來說,我對這次測試發現的東西非常滿意。如果我能重做一些事情(或得到一筆錢來做這個計畫),我希望能做到以下幾點:
讓每個場景都能報告一些後設資料。例如,物件數量、材質等。
對 Jupyter+Pandas 有更好的了解。雖然我是一位 C++ 開發者,不是數據科學家,但我希望能了解如何更好地轉換測量結果,使其看起來更美觀。
找到一種在 Android 和 iOS 上執行自動化測試的方法。目前這兩個平台都不容易測試,這是一個很明顯的問題。
run_verfication_tests.py 更像是一個應用程式(而不是一個小指令碼)。
PNG 開始變得有些龐大,有一次我磁盤空間都不足了。無失真 WebP 作為渲染輸出可能更好。
比較更多的英特爾芯片,並使用更多的編譯器。
結論
如果你只是匆匆翻到結尾,以下是總結:
使用 GCC 可能會得到一些好處。
對蘋果芯片的影響不大。
不要在 Clang 和 MSVC 上使用 final。
這完全取決於你的配置/平台,請自主測試並衡量是否值得。
最後,就我個人而言,我應該不會用 C++ 的 final 關鍵字來提升效能,本文的測試結果說明了這種方式並不穩定。
推薦閱讀: