web-dev-qa-db-ja.com

SkylakeにVZEROUPPERがないと、なぜSSEコードは6倍遅くなりますか?

私はアプリケーションのパフォーマンスの問題を解明しようとしてきましたが、ついにそれを本当に奇妙な問題に絞り込みました。次のコードは、VZEROUPPER命令がコメント化されている場合、Skylake CPU(i5-6500)で6倍遅く実行されます。私はSandy BridgeとIvy Bridge CPUをテストしましたが、どちらのバージョンもVZEROUPPERの有無にかかわらず同じ速度で動作します。

今、私はVZEROUPPERが何をするかについてかなり良い考えを持っています。VEXでコード化された命令がなく、それらを含む可能性のある関数への呼び出しがない場合、このコードはまったく問題にならないと思います。他のAVX対応のCPUにはないという事実は、これをサポートしているようです。 インテル®64およびIA-32アーキテクチャー最適化リファレンスマニュアル の表11-2も同様です

では、何が起こっているのでしょうか。

私が残した唯一の理論は、CPUにバグがあり、「AVXレジスタの上半分を保存する」手順を誤ってトリガーしないというものです。それとも奇妙な何か。

これはmain.cppです。

#include <immintrin.h>

int slow_function( double i_a, double i_b, double i_c );

int main()
{
    /* DAZ and FTZ, does not change anything here. */
    _mm_setcsr( _mm_getcsr() | 0x8040 );

    /* This instruction fixes performance. */
    __asm__ __volatile__ ( "vzeroupper" : : : );

    int r = 0;
    for( unsigned j = 0; j < 100000000; ++j )
    {
        r |= slow_function( 
                0.84445079384884236262,
                -6.1000481519580951328,
                5.0302160279288017364 );
    }
    return r;
}

これはslow_function.cppです:

#include <immintrin.h>

int slow_function( double i_a, double i_b, double i_c )
{
    __m128d sign_bit = _mm_set_sd( -0.0 );
    __m128d q_a = _mm_set_sd( i_a );
    __m128d q_b = _mm_set_sd( i_b );
    __m128d q_c = _mm_set_sd( i_c );

    int vmask;
    const __m128d zero = _mm_setzero_pd();

    __m128d q_abc = _mm_add_sd( _mm_add_sd( q_a, q_b ), q_c );

    if( _mm_comigt_sd( q_c, zero ) && _mm_comigt_sd( q_abc, zero )  )
    {
        return 7;
    }

    __m128d discr = _mm_sub_sd(
        _mm_mul_sd( q_b, q_b ),
        _mm_mul_sd( _mm_mul_sd( q_a, q_c ), _mm_set_sd( 4.0 ) ) );

    __m128d sqrt_discr = _mm_sqrt_sd( discr, discr );
    __m128d q = sqrt_discr;
    __m128d v = _mm_div_pd(
        _mm_shuffle_pd( q, q_c, _MM_SHUFFLE2( 0, 0 ) ),
        _mm_shuffle_pd( q_a, q, _MM_SHUFFLE2( 0, 0 ) ) );
    vmask = _mm_movemask_pd(
        _mm_and_pd(
            _mm_cmplt_pd( zero, v ),
            _mm_cmple_pd( v, _mm_set1_pd( 1.0 ) ) ) );

    return vmask + 1;
}

関数は、clangを使用して次のようにコンパイルされます。

 0:   f3 0f 7e e2             movq   %xmm2,%xmm4
 4:   66 0f 57 db             xorpd  %xmm3,%xmm3
 8:   66 0f 2f e3             comisd %xmm3,%xmm4
 c:   76 17                   jbe    25 <_Z13slow_functionddd+0x25>
 e:   66 0f 28 e9             movapd %xmm1,%xmm5
12:   f2 0f 58 e8             addsd  %xmm0,%xmm5
16:   f2 0f 58 ea             addsd  %xmm2,%xmm5
1a:   66 0f 2f eb             comisd %xmm3,%xmm5
1e:   b8 07 00 00 00          mov    $0x7,%eax
23:   77 48                   ja     6d <_Z13slow_functionddd+0x6d>
25:   f2 0f 59 c9             mulsd  %xmm1,%xmm1
29:   66 0f 28 e8             movapd %xmm0,%xmm5
2d:   f2 0f 59 2d 00 00 00    mulsd  0x0(%rip),%xmm5        # 35 <_Z13slow_functionddd+0x35>
34:   00 
35:   f2 0f 59 ea             mulsd  %xmm2,%xmm5
39:   f2 0f 58 e9             addsd  %xmm1,%xmm5
3d:   f3 0f 7e cd             movq   %xmm5,%xmm1
41:   f2 0f 51 c9             sqrtsd %xmm1,%xmm1
45:   f3 0f 7e c9             movq   %xmm1,%xmm1
49:   66 0f 14 c1             unpcklpd %xmm1,%xmm0
4d:   66 0f 14 cc             unpcklpd %xmm4,%xmm1
51:   66 0f 5e c8             divpd  %xmm0,%xmm1
55:   66 0f c2 d9 01          cmpltpd %xmm1,%xmm3
5a:   66 0f c2 0d 00 00 00    cmplepd 0x0(%rip),%xmm1        # 63 <_Z13slow_functionddd+0x63>
61:   00 02 
63:   66 0f 54 cb             andpd  %xmm3,%xmm1
67:   66 0f 50 c1             movmskpd %xmm1,%eax
6b:   ff c0                   inc    %eax
6d:   c3                      retq   

生成されたコードはgccでは異なりますが、同じ問題を示しています。古いバージョンのインテルコンパイラーは、関数のさらに別のバリエーションを生成しますが、これはmain.cppは、おそらくどこかでVZEROUPPERを実行することになる独自のライブラリの一部を初期化するための呼び出しを挿入するため、Intelコンパイラでは構築されません。

そしてもちろん、全体がAVXサポートを使用して構築され、組み込み関数がVEXコード化命令に変換されている場合も、問題はありません。

私はlinuxでperfを使用してコードをプロファイリングしようとしましたが、ランタイムのほとんどは通常1〜2の命令で実行されますが、プロファイルするコードのバージョン(gcc、clang、intel)によっては、常に同じではありません。 。関数を短くすると、パフォーマンスの違いが徐々になくなり、いくつかの命令が問題を引き起こしているように見えます。

編集:これはLinux用の純粋なアセンブリバージョンです。以下のコメント。

    .text
    .p2align    4, 0x90
    .globl _start
_start:

    #vmovaps %ymm0, %ymm1  # This makes SSE code crawl.
    #vzeroupper            # This makes it fast again.

    movl    $100000000, %ebp
    .p2align    4, 0x90
.LBB0_1:
    xorpd   %xmm0, %xmm0
    xorpd   %xmm1, %xmm1
    xorpd   %xmm2, %xmm2

    movq    %xmm2, %xmm4
    xorpd   %xmm3, %xmm3
    movapd  %xmm1, %xmm5
    addsd   %xmm0, %xmm5
    addsd   %xmm2, %xmm5
    mulsd   %xmm1, %xmm1
    movapd  %xmm0, %xmm5
    mulsd   %xmm2, %xmm5
    addsd   %xmm1, %xmm5
    movq    %xmm5, %xmm1
    sqrtsd  %xmm1, %xmm1
    movq    %xmm1, %xmm1
    unpcklpd    %xmm1, %xmm0
    unpcklpd    %xmm4, %xmm1

    decl    %ebp
    jne    .LBB0_1

    mov $0x1, %eax
    int $0x80

OK、コメントで疑われているように、VEXコード化された命令を使用すると速度が低下します。 VZEROUPPERを使用するとクリアされます。しかし、それでも理由はわかりません。

私が理解しているように、VZEROUPPERを使用しないと、古いSSE命令に移行するためのコストがかかりますが、それらの永続的なスローダウンではありません。特に、それほど大きなものではありません。)ループのオーバーヘッドを考慮すると、比率は少なくとも10倍、おそらくそれ以上になります。

アセンブリを少しいじってみましたが、float命令はdouble命令と同じくらい悪いです。単一の命令に問題を特定することもできませんでした。

34
Olivier

VEX以外の「混合」のペナルティが発生SSEとVEXでエンコードされた命令-可視アプリケーション全体がAVX命令を使用していないにもかかわらず!

Skylake以前は、このタイプのペナルティは一度だけtransitionペナルティでした。vexを使用するコードから使用しないコードに切り替えるとき、またはその逆です。つまり、VEXと非VEXを積極的に混在させない限り、過去に起こったことに対して継続的なペナルティを支払うことはありません。ただし、Skylakeでは、VEX以外のSSE命令は、さらに混合しなくても、継続的に高い実行ペナルティを支払う状態になります。

馬の口からまっすぐ、ここに図11-1 1 -古い(Skylake以前の)遷移図:

Pre-Skylake Transition Penalties

ご覧のとおり、すべてのペナルティ(赤い矢印)によって新しい状態になり、その時点でそのアクションを繰り返すペナルティはなくなりました。たとえば、256ビットのAVXを実行してdirty upper状態になり、レガシーSSEを実行した場合、one-timeへのペナルティを支払うことで、 preserved non-INIT upper状態ですが、その後ペナルティを支払う必要はありません。

Skylakeでは、すべてが図11-2ごとに異なります。

Skylake Penalties

全体的にペナルティは少ないですが、あなたのケースにとって決定的に重要なのは、それらの1つはセルフループです:レガシーを実行するペナルティSSE(ペナルティA図11-2)の命令dirty upper状態の命令は、その状態を維持します。それがあなたに起こることです-AVX命令は、あなたをダーティアッパー状態にします、これにより、すべてがさらに遅くなりますSSE実行が遅くなります。

新しい罰則についてIntelが言っていること(セクション11.3)は次のとおりです。

Skylakeマイクロアーキテクチャは、前の世代とは異なるステートマシンを実装して、SSEとAVX命令の混合に関連するYMM状態遷移を管理します。SSE「Modified and Unsaved」状態の命令ですが、個々のレジスタの上位ビットは保存されます。その結果、SSEとAVX命令を混在させると、使用されているデスティネーションレジスタのレジスタ依存性、およびデスティネーションレジスタの上位ビットに対する追加のブレンド操作。

したがって、ペナルティは明らかに非常に大きいです-上位ビットを常にブレンドしてそれらを保持する必要があり、非表示の上位ビットに依存しているため、明らかに独立して依存する命令も作成します。たとえば、_xorpd xmm0, xmm0_は、_xmm0_の以前の値への依存を解除しなくなりました。これは、実際には、xorpdによってクリアされない_ymm0_の非表示の上位ビットに依存しているためです。通常の分析からは予想されない非常に長い依存関係の連鎖が存在するようになるため、後者の影響はおそらくパフォーマンスを低下させます。

これは最悪のタイプのパフォーマンスの落とし穴の1つです。従来のアーキテクチャの動作/ベストプラクティスは、基本的に現在のアーキテクチャとは逆です。おそらく、ハードウェアアーキテクトが変更を加える正当な理由がありましたが、微妙なパフォーマンス問題のリストに別の「問題」が追加されただけです。

そのAVX命令を挿入し、VZEROUPPERでフォローアップしなかったコンパイラまたはランタイムに対してバグを報告します。

Update:OPの comment の下で、問題の(AVX)コードがランタイムリンカーldと-によって挿入されました バグ はすでに存在します。


1 Intelの 最適化マニュアル から。

39
BeeOnRope

私は(ハスウェルで)いくつかの実験をしました。クリーン状態とダーティー状態の間の遷移は高価ではありませんが、ダーティー状態はすべての非VEXベクトル操作を宛先レジスターの以前の値に依存させます。あなたの場合、たとえば、movapd %xmm1, %xmm5ymm5に誤った依存関係を持ち、順序が乱れた実行を防ぎます。これは、AVXコードの後に​​vzeroupperが必要な理由を説明しています。

17
A Fog