16

私は SIMD 組み込み関数の完全な初心者です。

基本的に、私は AVX2 組み込み ( Intel(R) Core(TM) i5-7500T CPU @ 2.70GHz) をサポートする CPU を持っています。std::vector<float>sizeの 2 の内積を計算する最速の方法を知りたい512です。

私はオンラインで掘り下げて、これとこれを見つけました。このスタックオーバーフロー質問は、次の関数を使用することを提案しています__m256 _mm256_dp_ps(__m256 m1, __m256 m2, const int mask);。ただし、これらはすべて、内積を実行するさまざまな方法を示唆しています。何が正しい(そして最速の)方法なのかわかりませんそれ。

特に、サイズ 512 のベクトルに対して内積を実行する最速の方法を探しています (ベクトルのサイズが実装に影響することがわかっているため)。

ご協力ありがとうございました

編集 1-mavx2 : gcc フラグについても少し混乱しています。これらの AVX2 関数を使用する場合、コンパイル時にフラグを追加する必要がありますか? -OFastまた、単純な内積の実装を作成した場合、gccはこれらの最適化を実行できますか (gcc フラグを使用する場合など)。

編集2 誰かが時間とエネルギーを持っているなら、完全な実装を書いていただければ幸いです。他の初心者もこの情報を高く評価していると確信しています。

4

1 に答える 1

16

_mm256_dp_ps2 ~ 4 要素の内積にのみ役立ちます。より長いベクトルの場合、ループで垂直 SIMD を使用し、最後にスカラーに縮小します。_mm256_dp_psandをループで使用すると_mm256_add_ps、はるかに遅くなります。


GCC と clang では、MSVC や ICC とは異なり、組み込み関数を使用する ISA 拡張機能を (コマンド ライン オプションで) 有効にする必要があります。


以下のコードは、おそらく CPU の理論上のパフォーマンス限界に近いものです。未テスト。

clang または でコンパイルしgcc -O3 -march=nativeます。(少なくとも が必要ですが、によって暗示されたオプション-mavx -mfmaも適切であり、その他のものも有効になります。チューニング オプションは、FMA を使用するほとんどの CPU で効率的にコンパイルするために重要です。具体的には、gcc が _mm256_loadu_pd を単一の vmovupd として解決しないのはなぜですか? )-mtune-march-mpopcntarch=native-mno-avx256-split-unaligned-load

または、MSVC でコンパイルします。-O2 -arch:AVX2

#include <immintrin.h>
#include <vector>
#include <assert.h>

// CPUs support RAM access like this: "ymmword ptr [rax+64]"
// Using templates with offset int argument to make easier for compiler to emit good code.

// Multiply 8 floats by another 8 floats.
template<int offsetRegs>
inline __m256 mul8( const float* p1, const float* p2 )
{
    constexpr int lanes = offsetRegs * 8;
    const __m256 a = _mm256_loadu_ps( p1 + lanes );
    const __m256 b = _mm256_loadu_ps( p2 + lanes );
    return _mm256_mul_ps( a, b );
}

// Returns acc + ( p1 * p2 ), for 8-wide float lanes.
template<int offsetRegs>
inline __m256 fma8( __m256 acc, const float* p1, const float* p2 )
{
    constexpr int lanes = offsetRegs * 8;
    const __m256 a = _mm256_loadu_ps( p1 + lanes );
    const __m256 b = _mm256_loadu_ps( p2 + lanes );
    return _mm256_fmadd_ps( a, b, acc );
}

// Compute dot product of float vectors, using 8-wide FMA instructions.
float dotProductFma( const std::vector<float>& a, const std::vector<float>& b )
{
    assert( a.size() == b.size() );
    assert( 0 == ( a.size() % 32 ) );
    if( a.empty() )
        return 0.0f;

    const float* p1 = a.data();
    const float* const p1End = p1 + a.size();
    const float* p2 = b.data();

    // Process initial 32 values. Nothing to add yet, just multiplying.
    __m256 dot0 = mul8<0>( p1, p2 );
    __m256 dot1 = mul8<1>( p1, p2 );
    __m256 dot2 = mul8<2>( p1, p2 );
    __m256 dot3 = mul8<3>( p1, p2 );
    p1 += 8 * 4;
    p2 += 8 * 4;

    // Process the rest of the data.
    // The code uses FMA instructions to multiply + accumulate, consuming 32 values per loop iteration.
    // Unrolling manually for 2 reasons:
    // 1. To reduce data dependencies. With a single register, every loop iteration would depend on the previous result.
    // 2. Unrolled code checks for exit condition 4x less often, therefore more CPU cycles spent computing useful stuff.
    while( p1 < p1End )
    {
        dot0 = fma8<0>( dot0, p1, p2 );
        dot1 = fma8<1>( dot1, p1, p2 );
        dot2 = fma8<2>( dot2, p1, p2 );
        dot3 = fma8<3>( dot3, p1, p2 );
        p1 += 8 * 4;
        p2 += 8 * 4;
    }

    // Add 32 values into 8
    const __m256 dot01 = _mm256_add_ps( dot0, dot1 );
    const __m256 dot23 = _mm256_add_ps( dot2, dot3 );
    const __m256 dot0123 = _mm256_add_ps( dot01, dot23 );
    // Add 8 values into 4
    const __m128 r4 = _mm_add_ps( _mm256_castps256_ps128( dot0123 ), _mm256_extractf128_ps( dot0123, 1 ) );
    // Add 4 values into 2
    const __m128 r2 = _mm_add_ps( r4, _mm_movehl_ps( r4, r4 ) );
    // Add 2 lower values into the final result
    const __m128 r1 = _mm_add_ss( r2, _mm_movehdup_ps( r2 ) );
    // Return the lowest lane of the result vector.
    // The intrinsic below compiles into noop, modern compilers return floats in the lowest lane of xmm0 register.
    return _mm_cvtss_f32( r1 );
}

さらなる改善の可能性:

  1. 4 ではなく 8 ベクトルでアンロールします。gcc 9.2 asm outputを確認しましたが、コンパイラは利用可能な 16 個のベクトル レジスタのうち 8 個しか使用しませんでした。

  2. 両方の入力ベクトルが整列されていることを確認します。たとえば、msvc で_aligned_malloc/を呼び出すカスタム アロケータを使用するか、gcc と clang で/を呼び出します。次に、に置き換えます。_aligned_freealigned_allocfree_mm256_loadu_ps_mm256_load_ps


-ffast-math単純なスカラー ドット積を自動ベクトル化するには、OpenMP SIMD or (によって暗示される)も必要であり-Ofast、コンパイラが FP 数学を連想として扱わないようにします (丸めのため)。ただし、GCC は自動ベクトル化の際に複数のアキュムレータを使用しないため、アンロールしたとしても、ロード スループットではなく、FMA レイテンシのボトルネックになります。

(FMA あたり 2 ロードは、このコードのスループットのボトルネックが実際の FMA 操作ではなく、ベクトル ロードであることを意味します)。

于 2019-12-27T01:55:25.207 に答える