基于C++ 語言庫的GCC和Clang編譯器基準測試報告(ETL)
從我使用 C++ 代碼完成了不同編譯器的基準測試到現在,已經有一段時間了。由于我最近發布了 ETL 項目的 1.1 版(一個具有表達式模板的優化矩陣/向量計算庫),所以我決定使用它作為我的基準測試的基版本。它是一個帶有大量模板的 C++ 14 庫。我要編譯完整的測試套件(124 個測試用例)。這是直接在最新版本(1.1)的代碼上完成的。我將在調試模式下編譯一次,并在 release_debug(release + debug 符號和斷言)下進行一次編譯,并記錄每個編譯器的執行時間。該測試將使用支持 ETL 中的每個選項的配置進行編譯,以此計算最大的編譯時間。每次編譯都使用四個線程(make -j4)。 我還做了一些基準測試,以了解每個編譯器生成的代碼間的運行時的性能差異。基準測試將編譯為發布模式,并記錄其編譯時間。
我將測試以下編譯器:
- GCC-4.9.4
- GCC-5.4.0
- GCC-6.3.0
- GCC-7.1.0
- clang-3.9.1
- clang-4.0.1
- zapcc-1.0 (商業版,基于 clang-5.0 主分支)
所有這些都是直接使用 Portage(Gentoo 軟件包管理器)安裝的,除了從源代碼安裝的 clang-4.0.1 以及沒有 Gentoo 軟件包的 zapcc。由于 Gentoo 上的 clang 包不支持多進程,所以我不得不從源代碼中安裝一個版本,從包管理器中安裝另一個版本。這也是我測試較少版本的 clang 的原因,更實用點。
為了實現這些測試的目標,所有編譯器都使用了完全相同的選項。通常,我在 clang 上使用比 GCC 更多不同的選項(主要是考慮到在 clang 上更嚴格的向量化選項)。這可能不會使得每個編譯器達到最佳性能,但可以對使用默認優化級別的輸出之間進行比較。以下是使用的主要選項:
- 調試模式下: -g
- 發布+調試模式下: -g -O2
- 發布模式下: -g -O3 -DNDEBUG -fomit-frame-pointer
每種情況都啟用了許多警告,ETL 選項也是一樣的。
所有的測試結果都是運行在 Intel Core i7-2600(Sandy Bridge ...)@ 3.4GHz 上的 Gentoo 機器上收集的,該機器具有 4 核和 8 線程、12G 的 RAM 和一個 SSD。我盡可能地從干擾項中分離出基準數據,并且我的基準代碼是相當健全的,但是有些結果可能并不完全準確。此外,一些基準測試是在使用多線程,這可能會增加一些干擾和不可預測性。當我對測試結果不太確定時,我會多次運行基準測試以對此確認,并且總體而言,我對結果很有信心。
編譯時間
讓我們從編譯器自身的性能結果開始:
注: 在 Release_Debug 和 Benchmark,我對 zapcc 只使用了三個線程, 因為 12Go 的內存對于四個線程并不足夠。
不同的編譯器之間有一些非常重要的區別。總地來說,clang-4.0.1 是迄今為止調試模式下最快的免費編譯器。然而,當測試代碼被添加優化選項加以編譯,clang 就落后了。在調試模式和發布模式下,clang-4.0.1 比 clang-3.9.1 快得多,這一點令人印象深刻。在這一點上 clang 團隊干得不錯!這些優化,使得 clang-4.0.1 在發布模式下幾乎與 gcc-7.1 平分秋色。對于 GCC 來說,優化的成本似乎一直在顯著地上升。然而, GCC 7.1 似乎使得優化加快,也使得標準編譯快了許多。如果我們考慮 zapcc,這是調試模式下最快的編譯器,但它的速度在發布模式下比幾個 gcc 版本要慢。
總地來說,我對 clang-4.0.1 的性能印象深刻,它看起來真快!在不久的將來,我一定會用這個新版本做更多的測試。看到 g++-7.1 的編譯速度確實快于 gcc-6.3,也同樣令人欣慰。然而,對優化而言,最快的 gcc 版本仍然是 gcc-4.9.4 ,這已經是一個對 C++ 標準低支持的老版本。
運行時性能
現在來看看生成的代碼的質量。對于一些基準測試,我已經包含了兩個版本的算法。 std 是最簡單的算法(原始版),vec 是手工向量化和優化的實現版本。所有的測試都是在單精度浮點上完成的。
點乘
運行的第一個基準是計算兩個向量之間的點積。讓我們先看看原始版的性能:
不同的編譯器之間的差異不是很大。基于 clang 的編譯器似乎是生成速度最快代碼的編譯器。有趣的是,gcc-6.3 似乎在大數據量的容器中有一個很大的性能衰減,但在 gcc-7.1 中已經解決了。
如果我們查看優化版本的結果,其中差異更小。同樣,基于 clang 的編譯器生成的可執行文件是最快的,但緊隨其后的是 gcc,除了 gcc-6.3 之外,我們仍然可以看到與之前相同的性能衰退。
Logistic Sigmoid
下一個測試是檢查 sigmoid 操作的性能。在這種情況下,庫的評估者將嘗試使用并行化和向量化來計算。讓我們看看不同編譯器的開銷如何:
有趣的是,我們可以看到,gcc-7.1 在少量數據時是最快的,而 clang-4.0 最適合生成較大數據時的代碼。然而,除了最大的向量大小,差異并不是很明顯。顯然,zapcc(或 clang-5.0)有一個回歸,因為它比 clang-4.0 慢,并與 clang-3.9 相同速度。
Y = Alpha * X + Y (axpy)
第三個基準是著名的 axpy(y = alpha * x + y)。這是完全由庫中的表達式模板決定的,沒有使用特定的算法。我們來看看結果:
即使是最大的 vector,一旦向量化和并行化之后,這也是一個非常快速的操作。以這種速度,觀察到的一些差異可能不是很重要。再次,基于 clang 的版本是這段代碼中最快的版本,但差異還是很小。在 gcc-7.1 中似乎還有一點回歸,但這也是相當小的。
矩陣間的乘法 (GEMM)
下一個基準測試是測試 Matrix-Matrix 乘法的性能,這是在 BLAS 命名中被稱為 GEMM 的操作。在這種情況下,我們同時測試原始的和優化的向量化實現。為了節省一些橫向空間,我把表分成兩部分。
這一次,不同編譯器之間的性能差異非常大。clang 編譯器現在是大幅度領先,其中 clang-4.0 是他們中最快的(也有不錯的提升幅度)。事實上,clang-4.0.1 生成代碼,平均比最好的 GCC 編譯器生成的代碼速度快兩倍。非常有趣的是,從 GCC-5.4 開始,我們可以看到一個巨大的性能衰退,而且這種衰退還在 GCC-7.1 中。事實上,測試版本中最好的 GCC 版本依然是 GCC-4.9.4。Clang 真的在編譯 GEMM 代碼方面做得很好。
至于優化的版本,這兩大家族是相反的。的確,GCC 在這方面做的工作比 clang 要好,盡管差距沒有以前那么大了,但還是值得注意。我們還是可以觀察到 GCC 版本中的一個小回歸,因為 4.9 版本依然是最快的。至于 clang 版本,似乎 clang-5.0 (在 zapcc 中使用)在這個例子中有了很多的性能改進。
在這個例子中矩陣相乘,它是非常令人印象深刻的,優化與非優化代碼在性能上差異非常巨大。并且,令人印象深刻的是,每種類型的編譯器都有它們的長處,clang 看起來更適合處理沒優化過的代碼,而 GCC 更適合處理向量化的代碼。