Highway 是一個 C++ 函式庫,提供可移植的 SIMD/向量內在函式。
文件
以前在 Apache 2 下獲得許可,現在在 Apache 2 / BSD-3 下獲得雙重許可。
我們對高效能軟體充滿熱情。我們看到 CPU(伺服器、行動裝置、桌上型電腦)有巨大的未開發潛力。 Highway 適合想要可靠且經濟地突破軟體可能性界限的工程師。
CPU 提供 SIMD/向量指令,可將相同的操作套用至多個資料項。這可以減少能源使用,例如五倍,因為執行的指令更少。我們也常看到5-10 倍的加速。
Highway 根據以下指導原則使 SIMD/向量程式設計變得實用且可行:
滿足您的期望:Highway 是一個 C++ 函式庫,具有精心選擇的函數,可以很好地對應到 CPU 指令,而無需進行大量的編譯器轉換。與自動向量化相比,產生的程式碼對於程式碼變更/編譯器更新更具可預測性和穩健性。
適用於廣泛使用的平台:Highway支援五種架構;相同的應用程式程式碼可以針對各種指令集,包括具有「可擴展」向量的指令集(編譯時大小未知)。 Highway 僅需要 C++11,並支援四個系列的編譯器。如果您想在其他平台上使用 Highway,請提出問題。
部署靈活:使用 Highway 的應用程式可以在異質雲端或用戶端裝置上執行,在運行時選擇最佳的可用指令集。或者,開發人員可以選擇以單一指令集為目標,而無需任何執行時間開銷。在這兩種情況下,除了將HWY_STATIC_DISPATCH
與HWY_DYNAMIC_DISPATCH
交換加上一行程式碼之外,應用程式碼是相同的。另請參閱@kfjahnke 對調度的介紹。
適用於各種領域:Highway提供了廣泛的操作集,用於影像處理(浮點)、壓縮、視訊分析、線性代數、密碼學、排序和隨機生成。我們認識到新的用例可能需要額外的操作,並且很樂意在有意義的地方添加它們(例如,在某些架構上沒有效能懸崖)。如果您想討論,請提出問題。
獎勵資料並行設計:Highway 提供了 Gather、MaskedLoad 和 FixTag 等工具來加速遺留資料結構。然而,最大的收益是透過為可擴展向量設計演算法和資料結構來釋放的。有用的技術包括批次、數組結構佈局和對齊/填充分配。
我們推薦以下資源作為入門資源:
使用編譯器資源管理器的線上演示:
我們觀察到,透過 sourcegraph.com 找到的以下開源專案中引用了 Highway。大多數是 GitHub 儲存庫。如果您想新增您的項目或直接連結到該項目,請隨時提出問題或透過以下電子郵件與我們聯絡。
其他
如果您想取得 Highway,除了從這個 GitHub 儲存庫複製或將其用作 Git 子模組之外,您還可以在以下套件管理器或儲存庫中找到它:
另請參閱 https://repology.org/project/highway-simd-library/versions 上的清單。
Highway 支援 24 個目標,按平台字母順序列出:
EMU128
、 SCALAR
;NEON_WITHOUT_AES
、 NEON
、 NEON_BF16
、 SVE
、 SVE2
、 SVE_256
、 SVE2_128
;Z14
、 Z15
;PPC8
(v2.07)、 PPC9
(v3.0)、 PPC10
(v3.1B,由於編譯器錯誤尚不支持,請參閱#1207;還需要 QEMU 7.2);RVV
(1.0);WASM
、 WASM_EMU256
( wasm128 的 2x 展開版本,如果定義了HWY_WANT_WASM2
則啟用。這將繼續受支持,直到它可能被未來版本的 WASM 取代。);SSE2
SSSE3
(~英特爾酷睿)SSE4
(~Nehalem,也包括 AES + CLMUL)。AVX2
(~Haswell,也包括 BMI2 + F16 + FMA)AVX3
(~Skylake、AVX-512F/BW/CD/DQ/VL)AVX3_DL
(~Icelake,包括 BitAlg + CLMUL + GFNI + VAES + VBMI + VBMI2 + VNNI + VPOPCNT;需要透過定義HWY_WANT_AVX3_DL
來選擇加入,除非編譯為靜態調度),AVX3_ZEN4
(與 AVX3_DL 類似,但針對 AMD Zen4 進行了最佳化;如果編譯靜態調度,則需要透過定義HWY_WANT_AVX3_ZEN4
來選擇加入,但預設啟用執行時間調度),AVX3_SPR
(~藍寶石急流,含 AVX-512FP16)我們的政策是,除非另有說明,只要目標可以與目前支援的 Clang 或 GCC 進行(交叉)編譯,並使用 QEMU 進行測試,就將繼續受支援。如果目標可以使用 LLVM 主幹進行編譯並使用我們的 QEMU 版本進行測試而無需額外的標誌,那麼它就有資格包含在我們的持續測試基礎設施中。否則,將在發布之前使用 Clang 和 GCC 的選定版本/配置對目標進行手動測試。
SVE 最初是使用 farm_sve 進行測試的(請參閱致謝)。
Highway 版本旨在遵循 semver.org 系統 (MAJOR.MINOR.PATCH),在向後相容新增後增加 MINOR,在向後相容修復後增加 PATCH。我們建議使用版本(而不是 Git 提示),因為它們經過了更廣泛的測試,請參見下文。
目前版本 1.0 表示更加重視向後相容性。使用已記錄功能的應用程式將與具有相同主版本號的未來更新保持相容。
持續整合測試使用最新版本的 Clang(在本機 x86 上運行,或用於 RISC-V 和 Arm 的 QEMU)和 MSVC 2019(v19.28,在本機 x86 上運行)建置。
在發布之前,我們還使用 Clang 和 GCC 在 x86 上進行測試,並透過 GCC 交叉編譯在 Armv7/8 上進行測試。詳細資訊請參閱測試過程。
contrib
目錄包含與 SIMD 相關的實用程式:具有對齊行的圖像類別、數學庫(已實現 16 個函數,主要是三角函數)以及用於計算點積和排序的函數。
如果您只需要 x86 支持,您也可以使用 Agner Fog 的 VCL 向量類別庫。它包含許多功能,包括完整的數學庫。
如果您有使用 x86/NEON 內部函數的現有程式碼,您可能會對 SIMDe 感興趣,它使用其他平台的內部函數或自動向量化來模擬這些內部函數。
該專案使用CMake來產生和建置。在基於 Debian 的系統中,您可以透過以下方式安裝它:
sudo apt install cmake
Highway 的單元測試使用 googletest。預設情況下,Highway 的 CMake 在配置時下載此依賴項。您可以透過將HWY_SYSTEM_GTEST
CMake 變數設為 ON 並單獨安裝 gtest 來避免這種情況:
sudo apt install libgtest-dev
或者,您可以定義HWY_TEST_STANDALONE=1
並刪除每個 BUILD 檔案中所有出現的gtest_main
,然後測試避免對 GUnit 的依賴。
執行交叉編譯測試需要作業系統的支持,在 Debian 上由qemu-user-binfmt
軟體包提供。
若要將 Highway 建置為共用程式庫或靜態函式庫(取決於 BUILD_SHARED_LIBS),可以使用標準 CMake 工作流程:
mkdir -p build && cd build
cmake ..
make -j && make test
或者您可以執行run_tests.sh
(在 Windows 上為run_tests.bat
)。
Bazel 也支援構建,但它的使用/測試並不廣泛。
在針對 Armv7 進行建置時,目前編譯器的限制要求您將-DHWY_CMAKE_ARM7:BOOL=ON
新增至 CMake 命令列;參見#834 和#1032。據我們了解,消除這項限制的工作正在進行中。
官方不支援在 32 位元 x86 上構建,並且在預設情況下禁用 AVX2/3。請注意,johnplatts 已在 32 位元 x86(包括 AVX2/3)、GCC 7/8 和 Clang 8/11/12 上成功建置並執行了 Highway 測試。在 Ubuntu 22.04、Clang 11 和 12(但不是更高版本)上,需要額外的編譯器標誌-m32 -isystem /usr/i686-linux-gnu/include
。 Clang 10 及更早版本需要上述加上-isystem /usr/i686-linux-gnu/include/c++/12/i686-linux-gnu
。參見#1279。
高速公路現已在 vcpkg 中可用
vcpkg install highway
vcpkg 中的高速公路端口由 Microsoft 團隊成員和社區貢獻者保持最新。如果版本已過時,請在 vcpkg 儲存庫上建立問題或拉取要求。
您可以使用範例/中的benchmark
作為起點。
快速參考頁面簡要列出了所有操作及其參數,指令矩陣表示每個操作的指令數。
常見問題解答回答了有關可移植性、API 設計以及在哪裡可以找到更多資訊的問題。
我們建議盡可能使用完整的 SIMD 向量,以獲得最大的效能可攜性。要取得它們,請將ScalableTag
(或等效的HWY_FULL(float)
)標記傳遞給Zero/Set/Load
等函數。對於需要通道上限的用例,有兩種替代方案:
對於最多N
頻道,請指定CappedTag
或等效的HWY_CAPPED(T, N)
。實際通道數將N
向下舍入到最接近的 2 的冪,例如,如果N
為 5,則為 4;如果N
為 8,則為 8。仍然需要循環,因為向量實際上可能少於N
通道。
對於兩個N
通道的冪,請指定FixedTag
。支援的最大N
取決於目標,但保證至少為16/sizeof(T)
。
由於 ADL 限制,呼叫 Highway ops 的使用者代碼必須:
namespace hwy { namespace HWY_NAMESPACE {
;或者namespace hn = hwy::HWY_NAMESPACE; hn::Add()
;或者using hwy::HWY_NAMESPACE::Add;
。此外,每個呼叫 Highway ops 的函數(例如Load
)必須以HWY_ATTR
為前綴,或位於HWY_BEFORE_NAMESPACE()
和HWY_AFTER_NAMESPACE()
之間。 Lambda 函數目前在左大括號之前需要HWY_ATTR
。
請勿對 SIMD 向量使用命名空間範圍或static
初始化程序,因為這可能會在使用執行時間分派時導致 SIGILL,且編譯器會選擇為目前 CPU 不支援的目標編譯的初始化程序。相反,透過Set
初始化的常數通常應該是局部(const)變數。
使用 Highway 的程式碼入口點會根據使用靜態調度還是動態調度而略有不同。在這兩種情況下,我們建議頂級函數接收一個或多個指向數組的指針,而不是特定於目標的向量類型。
對於靜態調度, HWY_TARGET
將是HWY_BASELINE_TARGETS
中最好的可用目標,即那些允許編譯器使用的目標(請參閱快速參考)。 HWY_NAMESPACE
中的函數可以在定義它們的相同模組中使用HWY_STATIC_DISPATCH(func)(args)
進行呼叫。
對於動態調度,透過HWY_EXPORT
巨集產生函數指標表, HWY_DYNAMIC_DISPATCH(func)(args)
使用此巨集為目前 CPU 支援的目標呼叫最佳函數指標。如果定義了HWY_TARGET_INCLUDE
並且包含了foreach_target.h
,則會自動為HWY_TARGETS
中的每個目標編譯一個模組(請參閱快速參考)。請注意,第一次呼叫HWY_DYNAMIC_DISPATCH
或每次呼叫第一次呼叫HWY_DYNAMIC_POINTER
傳回的指標都會涉及一些 CPU 偵測開銷。您可以透過在呼叫HWY_DYNAMIC_*
之前呼叫以下命令來防止這種情況: hwy::GetChosenTarget().Update(hwy::SupportedTargets());
。
另請參閱 @kfjahnke 對動態調度的單獨介紹。
使用動態分派時, foreach_target.h
包含在翻譯單元(.cc 檔案)中,而不是標頭中。包含在多個翻譯單元之間共享的向量程式碼的標頭需要特殊的包含保護,例如以下內容取自examples/skeleton-inl.h
:
#if defined(HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_) == defined(HWY_TARGET_TOGGLE)
#ifdef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#undef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#else
#define HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#endif
#include "hwy/highway.h"
// Your vector code
#endif
按照慣例,我們將此類標頭命名為-inl.h
因為它們的內容(通常是函數模板)通常是內聯的。
應用程式應在啟用最佳化的情況下進行編譯。如果沒有內嵌 SIMD 程式碼,速度可能會降低 10 到-O2
倍。
對於 MSVC,我們建議使用/Gv
進行編譯,以允許非內聯函數在暫存器中傳遞向量參數。如果打算將 AVX2 目標與半角向量一起使用(例如,用於PromoteTo
),則使用/arch:AVX2
進行編譯也很重要。這似乎是在 MSVC 上可靠產生 VEX 編碼的 SSE 指令的唯一方法。有時,如果 MSVC 與 AVX 混合,MSVC 會產生 VEX 編碼的 SSE 指令,但並非總是如此,請參閱 DevCom-10618264。否則,混合 VEX 編碼的 AVX2 指令和非 VEX SSE 可能會導致嚴重的效能下降。不幸的是,使用/arch:AVX2
選項,生成的二進位檔案將需要 AVX2。請注意,clang 和 GCC 不需要這樣的標誌,因為它們支援特定於目標的屬性,我們使用這些屬性來確保為 AVX2 目標產生正確的 VEX 程式碼。
在對循環進行向量化時,一個重要的問題是是否以及如何處理未均勻劃分向量大小N = Lanes(d)
的多次迭代(“行程計數”,表示為count
)。例如,可能需要避免寫入超過數組末尾。
在本節中,讓T
表示元素類型,並且d = ScalableTag
。假設循環體以函數template
給出。
「條帶挖掘」是一種透過將循環轉換為外循環和內循環來向量化循環的技術,使得內循環中的迭代次數與向量寬度相符。然後,將內循環替換為向量運算。
Highway 提供了多種循環向量化策略:
確保所有輸入/輸出均已填滿。那麼(外)循環就是簡單的
for (size_t i = 0; i < count; i += N) LoopBody(d, i, 0);
這裡,不需要模板參數和第二個函數參數。
這是首選選項,除非N
為數千,向量運算以管道方式進行,且延遲較長。 90 年代的超級電腦就是這種情況,但現在ALU 很便宜,我們看到大多數實作將向量分成1、2 或4 個部分,因此即使我們不需要它們的所有通道,處理整個向量的成本也很小。事實上,這避免了在舊目標上進行預測或部分加載/儲存的(可能很大的)成本,並且不會重複程式碼。
處理整個向量並在最後一個向量中包含先前處理過的元素:
for (size_t i = 0; i < count; i += N) LoopBody(d, HWY_MIN(i, count - N), 0);
這是第二個首選選項,前提是count >= N
且LoopBody
是冪等的。有些元素可能會被處理兩次,但單一程式碼路徑和完整向量化通常是值得的。即使count < N
,將輸入/輸出填入N
通常也是有意義的。
使用 hwy/contrib/algo/transform-inl.h 中的Transform*
函數。這負責循環和餘數處理,您只需定義一個通用lambda 函數(C++14) 或仿函數,它接收來自輸入/輸出數組的當前向量,以及來自最多兩個額外輸入數組的可選向量,並傳回要寫入輸入/輸出數組的值。
以下是實作 BLAS 函數 SAXPY ( alpha * x + y
) 的範例:
Transform1(d, x, n, y, [](auto d, const auto v, const auto v1) HWY_ATTR {
return MulAdd(Set(d, alpha), v, v1);
});
如上所述處理整個向量,然後是標量循環:
size_t i = 0;
for (; i + N <= count; i += N) LoopBody(d, i, 0);
for (; i < count; ++i) LoopBody(CappedTag(), i, 0);
再次不需要模板參數和第二個函數參數。
這避免了重複程式碼,並且在count
很大時是合理的。如果count
很小,第二個循環可能比下一個選項慢。
如上所述處理整個向量,然後使用掩碼對修改後的LoopBody
進行一次呼叫:
size_t i = 0;
for (; i + N <= count; i += N) {
LoopBody(d, i, 0);
}
if (i < count) {
LoopBody(d, i, count - i);
}
現在,可以在LoopBody
內部使用模板參數和第三個函數參數,以非原子方式「混合」 v
的前num_remaining
通道與後續位置的記憶體先前內容: BlendedStore(v, FirstN(d, num_remaining), d, pointer);
。類似地, MaskedLoad(FirstN(d, num_remaining), d, pointer)
載入前num_remaining
元素並在其他通道中傳回零。
當無法確保填滿向量時,這是一個很好的預設值,但只有#if !HWY_MEM_OPS_MIGHT_FAULT
才是安全的!與標量循環相比,只需要一次最終迭代。兩個循環體增加的程式碼大小預計是值得的,因為它避免了除最終迭代之外的所有迭代中的屏蔽成本。
我們使用了Berenger Bramas的farm-sve;事實證明,它對於檢查 x86 開發機器上的 SVE 連接埠很有用。
這不是 Google 官方支援的產品。聯絡方式:[email protected]