3

さまざまな C++ コンパイラでの SSE コードの組み込みガイド付き最適化の結果に関するこの興味深い記事を読んだ後、特に投稿が数年前のものであるため、独自のテストを行うことにしました。私は MSVC を使用しましたが、これは投稿の作成者が実行したテストでは (VS 2010 バージョンではありましたが) 非常に貧弱であり、非常に基本的なシナリオに固執することにしました: XMM レジスタにいくつかの値をパックし、加算のような単純な操作を実行. この記事では、_mm_set_ps がスカラー移動とアンパック命令の奇妙なシーケンスに変換されているので、見てみましょう。

int _tmain(int argc, _TCHAR* argv[])
{
    __m128 foo = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);
    __m128 bar = _mm_set_ps(5.0f, 6.0f, 7.0f, 8.0f);
    __m128 ret = _mm_add_ps(foo, bar);

    // need to do something so vars won't be optimized out in Release
    float *f = (float *)(&ret);
    for (int i = 0; i < 4; i++) 
    {
        cout << "f[" << i << "] = " << f[i] << endl;
    }
}

次に、これをコンパイルしてデバッガー内で実行し、逆アセンブリを確認しました。

デバッグ:

__m128 foo = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);
00B814F0 movaps xmm0,xm​​mword ptr ds:[0B87840h]
00B814F7 movaps xmmword ptr [ebp-190h],xmm0
00B814FE movaps xmm0,xm​​mword ptr [ebp-190h]
00B81505 movaps xmmword ptr [foo],xmm0
= fmm_128 6.0f、7.0f、8.0f);
00B81509 movaps xmm0,xm​​mword ptr ds:[0B87850h]
00B81510 movaps xmmword ptr [ebp-170h],xmm0
00B81517 movaps xmm0,xm​​mword ptr [ebp-170h]
00B8151E movaps xmmword ptr [bar],xmm0
_ __mm );
00B81522 movaps xmm0,xm​​mword ptr [bar]
00B81526 movaps xmm1,xmmword ptr [foo]
00B8152A addps xmm1,xmm0
00B8152D movaps xmmword ptr [ebp-150h],xmm1
00B81534 movaps xmm0,xm​​mword ptr [ebp-150h]
00B8153B movaps xmmword ptr [ret],xmm0

完全に混乱しています。xmmword を __m128 に入れるのに 4 MOVAPS が必要なのはなぜですか? まず、データを xmm0 に入れます (どこかに格納されている 4 つの float 値のリテラルだと思いますが、それを見る方法はわかりません)。次に、ebp とオフセットが指す場所に xmm0 をコピーします。そこに xmm0 (?) があり、最後にそれを格納するはずの変数の場所に。なぜそんなに仕事をするのですか?

リリース: 今回は、コンパイラが xmmword をメモリに保存することをまったく回避することを期待していました。1 つを xmm0 に、もう 1 つを xmm1 に置き、ADDPS を実行し、結果をメモリに置き、それで完了します。代わりに私は得ました:

__m128 foo = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);
__m128 バー = _mm_set_ps(5.0f, 6.0f, 7.0f, 8.0f);
__m128 ret = _mm_add_ps(foo, bar);
003E1009 movaps xmm0,xm​​mword ptr ds:[3E2130h]
003E1010 push esi
003E1011 movaps xmmword ptr [esp+10h],xmm0

どうやら、ADDPSは必要ありません。コンパイラは、2 つの xmmwords がコンパイル時の定数であることに気付いたので、それらを追加して、結果をコードにリテラルとして入れただけだと思いますか? 私が知る限り、esi はそこでループ カウンターとして使用されているため、奇妙なプッシュはおそらく後続の for ループに関係しています。それでも、事前に計算されたリテラルをデータ セグメントから xmm0 に入れ、次にローカル変数 (esp+10h) に入れ、リテラルを直接使用しないのはなぜですか?

要約すると、デバッグ バージョンは予想以上に愚かでした (あるいは、何も得られなかったのかもしれません) が、リリース バージョンは予想外に巧妙でした。この動作を説明するコメントは大歓迎です。ありがとう。

編集:答えは非常に啓発的でしたが、コンパイラの出力を改善するためできることがあるかどうかを知りたいので、質問をこれについての説明を求めることから現在の形式に変更しています。

たとえば、コンパイラがfoobarをメモリに格納せず (追加後に必要ないため)、単に xmmN レジスタにロードしてそこに保持するようにガイドすることは可能でしょうか? もしかしてレトも?引用された記事の著者は、MSVC は「言われたことを正確に実行している」だけだと述べています。__asm ブロックを明示的に記述せずに、コードを改善する (読み取り: メモリ転送を回避する) 方法はありますか? ありがとう。

4

3 に答える 3

5

これは、コード ジェネレーターの動作の通常の副作用です。_mm_set_ps() には、実行する2 つの異なるジョブがあります。最初に、4 つの引数から __m128 値を構築する必要があります。簡単な方法を選択しましたが、次のように複雑になります。

float x = 1.0f;
__m128 foo = _mm_set_ps(x, 2.0f, 3.0f, 4.0f);

大幅に異なるコード生成を使用:

00C513DD  movss       xmm0,dword ptr ds:[0C5585Ch]  
00C513E5  movss       xmm1,dword ptr [x]  
00C513EA  movaps      xmm2,xmmword ptr ds:[0C55860h]  
00C513F1  unpcklps    xmm0,xmm1  
00C513F4  unpcklps    xmm2,xmm0  
00C513F7  movaps      xmmword ptr [ebp-100h],xmm2

2 番目の作業は、それを __m128 変数に移動することです。これは簡単です。

00C513FE  movaps      xmm0,xmmword ptr [ebp-100h]  
00C51405  movaps      xmmword ptr [foo],xmm0  

これがまだ最適化されていないのは、デバッグ ビルドでオプティマイザがオフになっているためです。コード ジェネレーターは最適化を試みません。それはその仕事ではありません。

確かに、オプティマイザはコンパイル時に結果を計算できました。これは、複雑な例でも機能します。これは既に見たとおりです。

00EE1284  movaps      xmm0,xmmword ptr ds:[0EE3260h]  
于 2013-02-14T05:16:20.913 に答える
1

リリース ビルドのコンパイル時の最適化については正しいです (ds:[3E2130h]オブジェクト ファイルを調べると、そこに追加された値が見つかります)。

はい、デバッグ バージョンは不必要な作業を行っているようですが、4 倍ではなく 2 倍にすぎません。

 movaps xmmword ptr [foo],xmmword ptr ds:[0B87840h]

存在しますが、存在しませんMOVAPS.2つのバリアントがあり、どちらもメモリからメモリへの移動を許可しません(これはx86の通常のケースです):

MOVAPS xmm1,xmm2/mem128       ; 0F 28 /r        [KATMAI,SSE]
MOVAPS xmm1/mem128,xmm2       ; 0F 29 /r        [KATMAI,SSE]

ds:[0B87840h]デバッグ アセンブリが行うことは、オブジェクト ファイルの.dataセクション (読み取り専用である可能性が最も高い)から xmmword を読み取り、それを[ebp-190h]と同様にスタックに置きますfoo

比較のために、gcc 4.7 は同様のパターンを示します。

movaps  xmm0, XMMWORD PTR .LC0[rip] # D.5374,
movaps  XMMWORD PTR [rbp-64], xmm0  # foo, D.5353
movaps  xmm0, XMMWORD PTR .LC1[rip] # D.5381,
movaps  XMMWORD PTR [rbp-48], xmm0  # bar, D.5354
movaps  xmm0, XMMWORD PTR [rbp-64]  # tmp79, foo
movaps  XMMWORD PTR [rbp-32], xmm0  # __A, tmp79
movaps  xmm0, XMMWORD PTR [rbp-48]  # tmp80, bar
movaps  XMMWORD PTR [rbp-16], xmm0  # __B, tmp80
movaps  xmm0, XMMWORD PTR [rbp-16]  # tmp81, __B
movaps  xmm1, XMMWORD PTR [rbp-32]  # tmp82, __A
addps   xmm0, xmm1  # D.5386, tmp82

これは、組み込み組み込み関数の実装方法に関係していると思います。たとえば、呼び出し時にレジスタ、スタック、または他の場所にある可能性のある引数を処理します_mm_add_ps__m128したがって、gcc/VC++ の組み込みコードを作成している場合は、値をロードするコードを最初に生成する必要があります。オプティマイザーを実行すると、不要なデータのプッシュが行われていることがすぐにわかります (ただし、オプティマイザーはデバッグ ビルドでは実行されません)。

于 2013-02-14T04:41:39.887 に答える
1

This is really a question about MSVC internals. To get a definite answer, you'd have to ask Microsoft.

One might speculate that the reason the Release build puts ret into a local variable is that you have taken its address. Taking the address of a variable means the compiler suddenly has to deal with memory rather than registers. Memory is much harder for a compiler because other places in the program may have pointers to that the optimizer must account for.

于 2013-02-14T04:52:21.330 に答える