當前位置: 妍妍網 > 碼農

使用 C++ 中的 final 關鍵字,到底能否提升效能?

2024-04-26碼農

使用 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 2Scene: book2::final_scene...Render took 58.587 seconds

可隨後又恢復了更改:

$ ./PSRayTracing -n 100 -j 2Scene: 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 關鍵字來提升效能,本文的測試結果說明了這種方式並不穩定。

    推薦閱讀: