highway - Highwayは、ポータブルSIMD/ベクトル組み込み関数を提供するC++ライブラリです。

(Performance-portable, length-agnostic SIMD with runtime dispatch)

Created at: 2019-09-06 20:41:23
Language: C++
License: Apache-2.0

効率的でパフォーマンスに優れたベクターソフトウェア

Highwayは、ポータブルSIMD/ベクトル組み込み関数を提供するC++ライブラリです。

どうして

私たちは高性能ソフトウェアに情熱を注いでいます。CPU(サーバー、モバイル、デスクトップ)には未開拓の大きな可能性があります。Highwayは、ソフトウェアで可能なことの限界を確実かつ経済的に押し上げたいエンジニア向けです。

どのように

CPUは、同じ操作を複数のデータ項目に適用するSIMD/ベクトル命令を提供します。これにより、実行される命令が少なくなるため、エネルギー使用量を5分の1に減らすことができますまた、 5〜10倍のスピードアップもよく見られます。

Highwayは、SIMD/ベクトルプログラミングを次の基本原則に従って実用的かつ実行可能にします。

期待どおりの動作:Highwayは、コンパイラを大幅に変換することなくCPU命令に適切にマップされる、慎重に選択された関数を備えたC++ライブラリです。結果のコードは、自動ベクトル化よりも予測可能で、コードの変更/コンパイラーの更新に対して堅牢です。

広く使用されているプラ​​ットフォームで動作:Highwayは4つのアーキテクチャをサポートしています。同じアプリケーションコードで、「スケーラブル」ベクトル(コンパイル時のサイズは不明)を含む8つの命令セットをターゲットにできます。HighwayはC++11のみを必要とし、コンパイラの4つのファミリをサポートします。他のプラットフォームでHighwayを使用したい場合は、問題を提起してください。

柔軟な導入:Highwayを使用するアプリケーションは、異種クラウドまたはクライアントデバイスで実行でき、実行時に利用可能な最適な命令セットを選択します。または、開発者は、実行時のオーバーヘッドなしで単一の命令セットをターゲットにすることを選択できます。どちらの場合も、アプリケーションコードは、プラス1行のコードと交換

HWY_STATIC_DISPATCH
することを除いて同じです。
HWY_DYNAMIC_DISPATCH

さまざまなドメインに適しています:Highwayは、画像処理(浮動小数点)、圧縮、ビデオ分析、線形代数、暗号化、並べ替え、およびランダム生成に使用される広範な操作セットを提供します。新しいユースケースでは追加の操作が必要になる可能性があることを認識しており、意味のある場所に追加できます(たとえば、一部のアーキテクチャではパフォーマンスの低下がありません)。話し合いたい場合は、問題を提出してください。

データ並列設計に報酬を与える:Highwayは、Gather、MaskedLoad、FixedTagなどのツールを提供して、レガシーデータ構造の高速化を可能にします。ただし、スケーラブルなベクトルのアルゴリズムとデータ構造を設計することで、最大のメリットが得られます。便利なテクニックには、バッチ処理、配列構造のレイアウト、整列/パディングされた割り当てなどがあります。

コンパイラエクスプローラを使用したオンラインデモ:

Highwayを使用するプロジェクト:(追加するには、問題を提起するか、以下の電子メールでお問い合わせください)

現在のステータス

ターゲット

サポートされているターゲット:スカラー、S-SSE3、SSE4、AVX2、AVX-512、AVX3_DL(〜Icelake、定義によるオプトインが必要

HWY_WANT_AVX3_DL
)、NEON(ARMv7およびv8)、SVE、SVE2、WASM SIMD、RISC-VV。

SVEは、最初にfarm_sveを使用してテストされました(謝辞を参照)。

バージョニング

Highwayのリリースは、semver.orgシステム(MAJOR.MINOR.PATCH)に従うことを目的としており、下位互換性のある追加の後にMINORをインクリメントし、下位互換性のある修正の後にPATCHをインクリメントします。より広範囲にテストされているため、(Gitのヒントではなく)リリースを使用することをお勧めします。以下を参照してください。

バージョン0.11は、他のプロジェクトで使用するのに十分安定していると見なされます。バージョン1.0は、下位互換性への注目が高まっていることを示しており、すべてのターゲットが機能を完備しているため、2022H1で計画されています。

テスト

継続的インテグレーションテストは、最新バージョンのClang(ネイティブx86で実行、Spike for RVV、およびQEMU for ARM)とVS2015のMSVC(ネイティブx86で実行)を使用して構築されます。

リリース前は、ClangとGCCを使用したx86、およびGCCクロスコンパイルとQEMUを介したARMv7/8でもテストしています。詳細については、 テストプロセスを参照してください。

関連モジュール

この

contrib
ディレクトリには、SIMD関連のユーティリティが含まれています。行が整列された画像クラス、数学ライブラリ(16個の関数がすでに実装されており、ほとんどが三角法)、および内積の計算と並べ替えのための関数です。

インストール

このプロジェクトでは、CMakeを使用して生成とビルドを行います。Debianベースのシステムでは、次の方法でインストールできます。

sudo apt install cmake

Highwayのユニットテストはgoogletestを使用します。デフォルトでは、HighwayのCMakeは構成時にこの依存関係をダウンロードします。これを無効にするには、

HWY_SYSTEM_GTEST
CMake変数をONに設定し、gtestを個別にインストールします。

sudo apt install libgtest-dev

Highwayを共有ライブラリまたは静的ライブラリ(BUILD_SHARED_LIBSに応じて)として構築するには、標準のCMakeワークフローを使用できます。

mkdir -p build && cd build
cmake ..
make -j && make test

run_tests.sh
または、 (
run_tests.bat
Windowsで)実行できます。

Bazelはビルドもサポートされていますが、それほど広く使用/テストされていません。

クイックスタート

benchmark
内部のexamples/を出発点として使用できます。

クイックリファレンスページには、すべての操作とそのパラメーターが簡単に一覧表示され、instruction_matrix は操作ごとの命令の数を示します。

パフォーマンスの移植性を最大化するために、可能な限り完全なSIMDベクトルを使用することをお勧めします。それらを取得するには、

ScalableTag<float>
(または同等 の
HWY_FULL(float)
)タグをなどの関数に渡し
Zero/Set/Load
ます。レーンに上限を必要とするユースケースには、次の2つの選択肢があります。

  • 最大

    N
    レーンの場合は、
    CappedTag<T, N>
    または同等の を指定します
    HWY_CAPPED(T, N)
    。実際のレーン数は、5の場合は4、8の場合は8
    N
    など、最も近い2の累乗に切り捨てられます。これは、狭い行列などのデータ構造に役立ちます。ベクトルは実際にはレーンより少ない場合があるため、ループは引き続き必要です。
    N
    N
    N

  • 正確に2

    N
    レーンの累乗の場合は、を指定します
    FixedTag<T, N>
    。サポートされる最大値
    N
    はターゲットによって異なりますが、少なくともであることが保証されています
    16/sizeof(T)

ADLの制限により、Highwayopsを呼び出すユーザーコードは次のいずれかである必要があります。

  • 中に住む
    namespace hwy { namespace HWY_NAMESPACE {
    ; また
  • 各opの前に
    namespace hn = hwy::HWY_NAMESPACE; hn::Add()
    ;などのエイリアスを付けます。また
  • 使用する操作ごとにusing-declarationsを追加します
    using hwy::HWY_NAMESPACE::Add;

さらに、Highway opsを呼び出す各関数には、接頭辞、、またはと

HWY_ATTR
の間
HWY_BEFORE_NAMESPACE()
に存在する必要があり
HWY_AFTER_NAMESPACE()
ます。ラムダ関数は現在
HWY_ATTR
、オープニングブレースの前に必要です。

Highwayを使用するコードへのエントリポイントは、静的ディスパッチを使用するか動的ディスパッチを使用するかによってわずかに異なります。

  • 静的ディスパッチの場合、は、コンパイラで使用が許可されている

    HWY_TARGET
    ターゲットの中で最も利用可能なターゲットになります (クイックリファレンスを参照)。内部の関数 は、それらが定義されているのと同じモジュール内を使用して呼び出すことができます。通常の関数でラップし、ヘッダーで通常の関数を宣言することにより、他のモジュールから関数を呼び出すことができます。
    HWY_BASELINE_TARGETS
    HWY_NAMESPACE
    HWY_STATIC_DISPATCH(func)(args)

  • 動的ディスパッチの場合、関数ポインターのテーブルは 、現在のCPUでサポートされているターゲットに最適な関数ポインターを呼び出すために

    HWY_EXPORT
    使用されるマクロを介して生成されます。が定義され、含まれている場合、モジュールは( クイックリファレンスを参照
    HWY_DYNAMIC_DISPATCH(func)(args)
    )の各ターゲットに対して自動的にコンパイルされます。
    HWY_TARGETS
    HWY_TARGET_INCLUDE
    foreach_target.h

コンパイラフラグ

アプリケーションは、最適化を有効にしてコンパイルする必要があります。インライン化しないと、SIMDコードが10〜100倍遅くなる可能性があります。clangおよびGCCの場合、

-O2
通常は十分です。

MSVCの場合、

/Gv
インライン化されていない関数がレジスタ内のベクトル引数を渡すことができるようにコンパイルすることをお勧めします。AVX2ターゲットを半値幅のベクトル(たとえば
PromoteTo
)と一緒に使用する場合は、を使用してコンパイルすることも重要
/arch:AVX2
です。これは、MSVCでVEXでエンコードされたSSE4命令を生成する唯一の方法のようです。そうしないと、VEXでエンコードされたAVX2命令と非VEX SSE4を混在させると、パフォーマンスが大幅に低下する可能性があります。残念ながら、結果のバイナリにはAVX2が必要になります。clangとGCCは、AVX2ターゲットの適切なVEXコード生成を保証するために使用するターゲット固有の属性をサポートしているため、このようなフラグは必要ないことに注意してください。

ストリップマイニングループ

ループをベクトル化するために、「ストリップマイニング」はループを外側のループと内側のループに変換し、反復回数は優先ベクトル幅に一致します。

このセクションで

T
は、要素タイプ、
d = ScalableTag<T>
処理
count
する要素の数、および
N = Lanes(d)
完全なベクトル内のレーンの数を示します。ループ本体が関数として与えられていると仮定します
template<bool partial, class D> void LoopBody(D d, size_t index, size_t max_n)

Highwayは、

N
分割する必要がないループを表現するいくつかの方法を提供し
count
ます。

  • すべての入力/出力が埋め込まれていることを確認します。次に、ループは単純です

    for (size_t i = 0; i < count; i += N) LoopBody<false>(d, i, 0);
    

    ここでは、テンプレートパラメータと2番目の関数の引数は必要ありません。

    N
    これは、数千にのぼり、ベクトル演算が長いレイテンシでパイプライン化されている場合を除いて、推奨されるオプションです。これは90年代のスーパーコンピューターの場合でしたが、現在ALUは安価であり、ほとんどの実装ではベクトルが1、2、または4つの部分に分割されるため、すべてのレーンが必要ない場合でも、ベクトル全体を処理するコストはほとんどありません。実際、これにより、古いターゲットでの予測または部分的なロード/ストアの(潜在的に大きな)コストが回避され、コードが重複しなくなります。

  • Transform*
    hwy / contrib / algo/transform-inl.hの関数を使用します。これにより、ループと残りの処理が処理され、入出力配列から現​​在のベクトルを受け取り、オプションで最大2つの追加の入力配列からベクトルを受け取る汎用ラムダ関数(C ++ 14)または関数を定義するだけで、次のようになります。入出力配列に書き込む値。

    alpha * x + y
    BLAS関数SAXPY( )を実装する例を次に示します。

    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<false>(d, i, 0);
    for (; i < count; ++i) LoopBody<false>(CappedTag<T, 1>(), i, 0);
    

    テンプレートパラメータと2番目の関数の引数も必要ありません。

    これにより、コードの重複が回避され、

    count
    が大きい場合は妥当です。
    count
    が小さい場合、2番目のループは次のオプションよりも遅くなる可能性があります。

  • 上記のようにベクトル全体を処理した後、

    LoopBody
    マスキングを使用して変更されたものを1回呼び出します。

    size_t i = 0;
    for (; i + N <= count; i += N) {
      LoopBody<false>(d, i, 0);
    }
    if (i < count) {
      LoopBody<true>(d, i, count - i);
    }
    

    これで、テンプレートパラメータと3番目の関数の引数を内部で使用して 、の最初のレーンを 後続の場所にある以前のメモリの内容と

    LoopBody
    非原子的に「ブレンド」することができます。同様に 、最初の要素をロードし 、他のレーンではゼロを返します。
    num_remaining
    v
    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つのループ本体からのコードサイズの増加は、最後の反復を除くすべてのマスキングのコストを回避するため、価値があると予想されます。

追加のリソース

謝辞

BerengerBramasによるfarm-sveを使用しました。x86開発マシンのSVEポートをチェックするのに役立つことが証明されています。

これは、公式にサポートされているGoogle製品ではありません。連絡先:janwas@google.com