Highway は、ポータブル SIMD/ベクター組み込み関数を提供する C++ ライブラリです。
ドキュメント
以前は Apache 2 でライセンスされていましたが、現在は Apache 2 / BSD-3 のデュアルライセンスとなっています。
私たちは高性能ソフトウェアに情熱を持っています。 CPU (サーバー、モバイル、デスクトップ) には未開発の大きな可能性があると考えています。 Highway は、ソフトウェアで可能なことの限界を確実かつ経済的に押し広げたいエンジニア向けです。
CPU は、複数のデータ項目に同じ演算を適用する SIMD/ベクトル命令を提供します。これにより、実行される命令が少なくなるため、エネルギー使用量を例えば5 倍に削減できます。 5 ~ 10 倍の高速化もよく見られます。
Highway では、次の基本原則に従って SIMD/ベクター プログラミングが実用的かつ実行可能になります。
期待どおりの機能: Highway は、大規模なコンパイラ変換を必要とせずに CPU 命令に適切にマップする、慎重に選択された関数を備えた C++ ライブラリです。結果として得られるコードは、自動ベクトル化よりも予測可能で、コード変更/コンパイラーの更新に対して堅牢です。
広く使用されているプラットフォームで動作: Highway は 5 つのアーキテクチャをサポートします。同じアプリケーション コードは、「スケーラブル」ベクトル (コンパイル時にサイズが不明) を含むさまざまな命令セットをターゲットにすることができます。 Highway には C++11 のみが必要で、4 ファミリのコンパイラがサポートされます。他のプラットフォームで Highway を使用したい場合は、問題を提起してください。
柔軟な導入: Highway を使用するアプリケーションは、実行時に利用可能な最適な命令セットを選択して、異種クラウドまたはクライアント デバイス上で実行できます。あるいは、開発者は、実行時のオーバーヘッドなしで単一の命令セットをターゲットにすることを選択することもできます。どちらの場合も、アプリケーション コードは、 HWY_STATIC_DISPATCH
をHWY_DYNAMIC_DISPATCH
に置き換える点と 1 行のコードを除いて同じです。 @kfjahnke によるディスパッチングの紹介も参照してください。
さまざまなドメインに適しています: Highway は、画像処理 (浮動小数点)、圧縮、ビデオ分析、線形代数、暗号化、並べ替え、ランダム生成に使用される広範な操作セットを提供します。新しいユースケースでは追加の操作が必要になる可能性があることを認識しており、合理的な場合には喜んで追加します (例: 一部のアーキテクチャではパフォーマンスの崖がない)。議論したい場合は、問題を提出してください。
データ並列設計に報いる: Highway は、従来のデータ構造の高速化を可能にする、Gather、MaskedLoad、FixedTag などのツールを提供します。ただし、スケーラブルなベクトルのアルゴリズムとデータ構造を設計することで最大のメリットが得られます。役立つテクニックには、バッチ処理、配列構造のレイアウト、整列/パディング割り当てなどがあります。
開始するには、次のリソースをお勧めします。
Compiler Explorer を使用したオンライン デモ:
私たちは、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
(~Sapphire Rapids、AVX-512FP16 を含む)私たちのポリシーは、特に指定がない限り、現在サポートされている Clang または GCC で (クロス) コンパイルでき、QEMU を使用してテストできる限り、ターゲットはサポートされ続けるということです。ターゲットを LLVM トランクでコンパイルし、追加のフラグなしで QEMU のバージョンを使用してテストできる場合、継続的テスト インフラストラクチャに含める資格があります。それ以外の場合、ターゲットは、Clang および GCC の選択されたバージョン/構成を使用してリリース前に手動でテストされます。
SVE は最初に farm_sve を使用してテストされました (謝辞を参照)。
ハイウェイ リリースは、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 への依存関係が回避されます。
クロスコンパイルされたテストを実行するには、OS のサポートが必要です。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 用にビルドする場合、現在のコンパイラの制限により、CMake コマンド ラインに-DHWY_CMAKE_ARM7:BOOL=ON
を追加する必要があります。 #834 と #1032 を参照してください。この制限を取り除く作業が進行中であることを理解しています。
32 ビット x86 でのビルドは正式にはサポートされておらず、AVX2/3 はデフォルトで無効になっています。 johnplatts は、AVX2/3 を含む 32 ビット x86、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 リポジトリで問題を作成するか、プル リクエストを作成してください。
開始点として、examples/ 内のbenchmark
を使用できます。
クイック リファレンス ページには、すべての操作とそのパラメーターが簡単にリストされており、instruction_matrix は操作ごとの命令の数を示します。
FAQ は、移植性、API 設計、および詳細情報の入手先に関する質問に答えます。
最大限のパフォーマンスの移植性を実現するために、可能な限り完全な SIMD ベクトルを使用することをお勧めします。これらを取得するには、 ScalableTag
(または同等のHWY_FULL(float)
) タグをZero/Set/Load
などの関数に渡します。レーンの上限が必要なユースケースには、次の 2 つの選択肢があります。
最大N
レーンの場合は、 CappedTag
または同等のHWY_CAPPED(T, N)
を指定します。実際のレーン数は、 N
が 5 の場合はN
、 N
が 8 の場合は 8 など、最も近い 2 のべき乗に切り捨てられます。これは、幅の狭い行列などのデータ構造に役立ちます。ベクトルの実際のレーン数はN
よりも少ない場合があるため、ループが依然として必要です。
正確に 2 のN
レーンの場合は、 FixedTag
を指定します。サポートされる最大のN
ターゲットによって異なりますが、少なくとも16/sizeof(T)
であることが保証されます。
ADL の制限により、Highway 操作を呼び出すユーザー コードは次のいずれかを行う必要があります。
namespace hwy { namespace HWY_NAMESPACE {
; 内に存在します。またはnamespace hn = hwy::HWY_NAMESPACE; hn::Add()
;またはusing hwy::HWY_NAMESPACE::Add;
。さらに、Highway 操作 ( Load
など) を呼び出す各関数には、 HWY_ATTR
というプレフィックスを付けるか、 HWY_BEFORE_NAMESPACE()
とHWY_AFTER_NAMESPACE()
の間に存在する必要があります。 Lambda 関数は現在、左中括弧の前にHWY_ATTR
が必要です。
SIMD ベクトルには名前空間スコープやstatic
イニシャライザを使用しないでください。ランタイム ディスパッチを使用するときに SIGILL が発生する可能性があり、コンパイラは現在の CPU でサポートされていないターゲット用にコンパイルされたイニシャライザを選択します。代わりに、 Set
によって初期化される定数は、通常、ローカル (const) 変数である必要があります。
Highway を使用するコードへのエントリ ポイントは、静的ディスパッチを使用するか動的ディスパッチを使用するかによって若干異なります。どちらの場合も、トップレベル関数はターゲット固有のベクトル型ではなく、配列への 1 つ以上のポインターを受け取ることをお勧めします。
静的ディスパッチの場合、 HWY_TARGET
HWY_BASELINE_TARGETS
の中で最も利用可能なターゲット、つまりコンパイラによる使用が許可されているターゲットになります (クイックリファレンスを参照)。 HWY_NAMESPACE
内の関数は、定義されている同じモジュール内でHWY_STATIC_DISPATCH(func)(args)
を使用して呼び出すことができます。関数を通常の関数でラップし、ヘッダーで通常の関数を宣言することにより、他のモジュールから関数を呼び出すことができます。
動的ディスパッチの場合、関数ポインタのテーブルはHWY_EXPORT
マクロを介して生成されます。このマクロは、現在の CPU でサポートされているターゲットに最適な関数ポインタを呼び出すためにHWY_DYNAMIC_DISPATCH(func)(args)
によって使用されます。 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 ~ 100 倍遅くなる可能性があります。clang と GCC の場合、通常は-O2
で十分です。
MSVC の場合、 /Gv
を使用してコンパイルして、非インライン関数がレジスタ内のベクトル引数を渡せるようにすることをお勧めします。 AVX2 ターゲットを半幅ベクトルとともに使用する場合 ( PromoteTo
など)、 /arch:AVX2
を使用してコンパイルすることも重要です。これが、MSVC で VEX エンコードされた SSE 命令を確実に生成する唯一の方法のようです。 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);
ここでは、テンプレート パラメーターと関数の 2 番目の引数は必要ありません。
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
が冪等である場合に、2 番目に推奨されるオプションです。一部の要素は 2 回処理される場合がありますが、通常は 1 つのコード パスと完全なベクトル化にそれだけの価値があります。 count < N
の場合でも、通常は入力/出力をN
までパディングするのが合理的です。
hwy/contrib/algo/transform-inl.h のTransform*
関数を使用します。これにより、ループと剰余の処理が行われ、入力/出力配列から現在のベクトルを受け取り、さらにオプションで最大 2 つの追加の入力配列からベクトルを受け取る汎用ラムダ関数 (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);
テンプレート パラメーターと 2 番目の関数引数も必要ありません。
これにより、コードの重複が回避され、 count
が大きい場合には合理的です。 count
が小さい場合、2 番目のループは次のオプションよりも遅くなる可能性があります。
上記のようにベクトル全体を処理し、続いてマスキングを使用して変更されたLoopBody
を 1 回呼び出します。
size_t i = 0;
for (; i + N <= count; i += N) {
LoopBody(d, i, 0);
}
if (i < count) {
LoopBody(d, i, count - i);
}
これで、テンプレート パラメーターと 3 番目の関数引数を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
!スカラー ループとは対照的に、必要な反復は最後の 1 回のみです。 2 つのループ本体によるコード サイズの増加は、最後の反復を除くすべての反復でのマスキングのコストを回避できるため、価値があることが期待されます。
ここでは、Berenger Bramas の farm-sve を使用しました。これは、x86 開発マシン上の SVE ポートをチェックするのに役立つことが証明されています。
これは正式にサポートされている Google 製品ではありません。連絡先: [email protected]