18

私は現在、 ( を使用) でオープン ソースの 3D アプリケーション フレームワークを開発しています。私自身の数学ライブラリは、XNA math ライブラリのように設計されており、 SIMDも念頭に置いています。しかし、現在はそれほど高速ではなく、メモリの整列に問題がありますが、それについては別の質問で詳しく説明します。

数日前、なぜ自分でSSEコードを書かなければならないのか自問自答しました。最適化がオンの場合、コンパイラは高度に最適化されたコードを生成することもできます。GCCの「ベクター拡張」も使えます。しかし、これは実際には移植可能ではありません。

独自の SSE コードを使用するとより制御できることはわかっていますが、多くの場合、この制御は必要ありません。

SSE の大きな問題の 1 つは、メモリ プールとデータ指向設計の助けを借りて、可能な限り制限された動的メモリの使用です。

今私の質問に:

  • ネイキッド SSE を使用する必要がありますか? カプセル化されているのかもしれません。

    __m128 v1 = _mm_set_ps(0.5f, 2, 4, 0.25f);
    __m128 v2 = _mm_set_ps(2, 0.5f, 0.25f, 4);
    
    __m128 res = _mm_mul_ps(v1, v2);
    
  • それとも、コンパイラが汚い仕事をするべきですか?

    float v1 = {0.5f, 2, 4, 0.25f};
    float v2 = {2, 0.5f, 0.25f, 4};
    
    float res[4];
    res[0] = v1[0]*v2[0];
    res[1] = v1[1]*v2[1];
    res[2] = v1[2]*v2[2];
    res[3] = v1[3]*v2[3];
    
  • または、追加のコードで SIMD を使用する必要がありますか? load追加の命令が必要な SIMD 演算を含む動的コンテナー クラスのようにstore

    Pear3D::Vector4f* v1 = new Pear3D::Vector4f(0.5f, 2, 4, 0.25f);
    Pear3D::Vector4f* v2 = new Pear3D::Vector4f(2, 0.5f, 0.25f, 4);
    
    Pear3D::Vector4f res = Pear3D::Vector::multiplyElements(*v1, *v2);
    

    上記の例では、 uses float[4]internal および uses storeand loadin each メソッドのような架空のクラスを使用していますmultiplyElements(...)。メソッドは SSE 内部を使用します。

SIMD と大規模なソフトウェア設計についてもっと学びたいので、別のライブラリを使用したくありません。ただし、ライブラリの例は大歓迎です。

PS:これは実際の問題ではなく、設計上の問題です。

4

3 に答える 3

14

SIMD 拡張機能を使用したい場合は、SSE 組み込み関数を使用することをお勧めします (もちろん、インライン アセンブリは絶対に避けてください。ただし、幸いなことに、インライン アセンブリを代替として挙げていませんでした)。しかし、きれいにするために、オーバーロードされた演算子を使用して、適切なベクター クラスにカプセル化する必要があります。

struct aligned_storage
{
    //overload new and delete for 16-byte alignment
};

class vec4 : public aligned_storage
{
public:
    vec4(float x, float y, float z, float w)
    {
         data_[0] = x; ... data_[3] = w; //don't use _mm_set_ps, it will do the same, followed by a _mm_load_ps, which is unneccessary
    }
    vec4(float *data)
    {
         data_[0] = data[0]; ... data_[3] = data[3]; //don't use _mm_loadu_ps, unaligned just doesn't pay
    }
    vec4(const vec4 &rhs)
        : xmm_(rhs.xmm_)
    {
    }
    ...
    vec4& operator*=(const vec4 v)
    {
         xmm_ = _mm_mul_ps(xmm_, v.xmm_);
         return *this;
    }
    ...

private:
    union
    {
        __m128 xmm_;
        float data_[4];
    };
};

ここで良いことは、無名共用体 (UB、私は知っていますが、これが機能しない SSE を備えたプラットフォームを教えてください) により、必要なときにいつでも標準の float 配列を使用できることです (のようにoperator[]または初期化 (使用しないでください_mm_set_ps) )、適切な場合にのみ SSE を使用してください。最新のインライン コンパイラでは、カプセル化はおそらく無料で行われます (VC10 が、このベクトル クラスを使用した一連の計算のために SSE 命令をどれだけうまく最適化したかにはかなり驚きました。一時メモリ変数への不要な移動の心配はありません。カプセル化なし)。

唯一の欠点は、整列されていないベクトルでは何も得られず、非 SSE よりも遅くなる可能性があるため、適切な整列に注意する必要があることです。しかし幸いなことに、のアラインメント要件は(および周囲のクラス)__m128に伝播するため、C++ が適切な手段を備えている動的割り当てを処理するだけで済みます。関数と関数 (もちろんすべてのフレーバー) が適切にオーバーロードされ、そこからベクトル クラスが派生するvec4基本クラスを作成する必要があります。タイプを標準コンテナで使用するには、それ以外の場合はグローバルを使用するため、もちろん特化する必要があります(おそらく完全を期すために) 。operator newoperator deletestd::allocatorstd::get_temporary_bufferstd::return_temporary_bufferoperator new

しかし、本当の欠点は、SSE ベクトルをメンバーとして持つクラスの動的割り当ても考慮する必要があることです。これは面倒かもしれませんが、これらのクラスを派生させて特殊化aligned_storage全体を配置することで、少し自動化することができます。std::allocator便利なマクロを台無しにします。

JamesWy​​nn は、これらの操作が特別な重い計算ブロック (テクスチャ フィルタリングや頂点変換など) にまとめられることが多いという点を指摘していますが、一方で、これらの SSE ベクトル カプセル化を使用しても、標準的なfloat[4]ベクトル クラスの実装よりもオーバーヘッドが生じることはありません。 . いずれにしても、計算を行うためにこれらの値をメモリからレジスタに取得する必要があります (x87 スタックまたはスカラー SSE レジスタ)。適切に配置されている場合は値)、並列で計算します。したがって、オーバーヘッドを誘発することなく、SSE 実装を非 SSE 実装に自由に切り替えることができます (私の推論が間違っている場合は訂正してください)。

しかし、メンバーとして持つすべてのクラスのアライメントを確保するのvec4が面倒な場合 (これがこのアプローチの唯一の欠点です)、計算に使用する特殊な SSE-vector 型を定義し、標準の非ストレージ用の SSE ベクトル。


編集:わかりました、ここで行われるオーバーヘッド引数を見てください (そして、最初は非常に合理的に見えます)。オーバーロードされた演算子のために、非常にきれいに見える一連の計算を見てみましょう:

#include "vec.h"
#include <iostream>

int main(int argc, char *argv[])
{
    math::vec<float,4> u, v, w = u + v;
    u = v + dot(v, w) * w;
    v = abs(u-w);
    u = 3.0f * w + v;
    w = -w * (u+v);
    v = min(u, w) + length(u) * w;
    std::cout << v << std::endl;
    return 0;
}

VC10がそれについてどう思うか見てみましょう:

...
; 6   :     math::vec<float,4> u, v, w = u + v;

movaps  xmm4, XMMWORD PTR _v$[esp+32]

; 7   :     u = v + dot(v, w) * w;
; 8   :     v = abs(u-w);

movaps  xmm3, XMMWORD PTR __xmm@0
movaps  xmm1, xmm4
addps   xmm1, XMMWORD PTR _u$[esp+32]
movaps  xmm0, xmm4
mulps   xmm0, xmm1
haddps  xmm0, xmm0
haddps  xmm0, xmm0
shufps  xmm0, xmm0, 0
mulps   xmm0, xmm1
addps   xmm0, xmm4
subps   xmm0, xmm1
movaps  xmm2, xmm3

; 9   :     u = 3.0f * w + v;
; 10   :    w = -w * (u+v);

xorps   xmm3, xmm1
andnps  xmm2, xmm0
movaps  xmm0, XMMWORD PTR __xmm@1
mulps   xmm0, xmm1
addps   xmm0, xmm2

; 11   :    v = min(u, w) + length(u) * w;

movaps  xmm1, xmm0
mulps   xmm1, xmm0
haddps  xmm1, xmm1
haddps  xmm1, xmm1
sqrtss  xmm1, xmm1
addps   xmm2, xmm0
mulps   xmm3, xmm2
shufps  xmm1, xmm1, 0

; 12   :    std::cout << v << std::endl;

mov edi, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A
mulps   xmm1, xmm3
minps   xmm0, xmm3
addps   xmm1, xmm0
movaps  XMMWORD PTR _v$[esp+32], xmm1
...

すべての命令とその使用法を徹底的に分析しなくても、不必要なロードやストアは最初にあるものを除いて (わかりました、初期化せずに残しました)、とにかく取得するために必要なものはないと確信しています。それらをメモリから計算レジスタに入れ、最後に、次の式のvように必要です。uandには何も格納しませwんでした。これは、これ以上使用しない一時変数にすぎないためです。すべてが完全にインライン化され、最適化されています。関数は s の後に実数を使用してdota を返しますが、XMM レジスタを離れることなく、次の乗算の内積の結果をシームレスにシャッフルすることさえできました。float_mm_store_sshaddps

したがって、通常はコンパイラの能力に少し懐疑的である私でさえ、独自の組み込み関数を特別な関数に手作りすることは、カプセル化によって得られるクリーンで表現力豊かなコードと比較して、実際には割に合わないと言わざるを得ません。イントリンをハンドラフティングすることで実際にいくつかの命令を省くことができるキラーな例を作成できるかもしれませんが、その場合も最初にオプティマイザの裏をかく必要があります。


編集:わかりました、Ben Voigt は、(おそらく問題ではない) メモリ レイアウトの非互換性以外に、共用体の別の問題を指摘しました。これは、厳密なエイリアシング規則に違反しており、コンパイラが異なる共用体メンバーにアクセスする命令を最適化する可能性があることです。コードが無効です。私はまだそれについて考えていません。実際に問題が発生するかどうかはわかりませんが、確かに調査が必要です。

本当に問題がある場合は、残念ながらdata_[4]メンバーを削除して、__m128単独で使用する必要があります。_mm_set_ps初期化のために、再びandに頼らなければなりません_mm_loadu_ps。はoperator[]もう少し複雑になり、 と の組み合わせが必要になる場合が_mm_shuffle_psあり_mm_store_ssます。ただし、const 以外のバージョンでは、代入を対応する SSE 命令に委譲するある種のプロキシ オブジェクトを使用する必要があります。その場合、コンパイラが特定の状況でこの追加のオーバーヘッドを最適化できる方法を調査する必要があります。

または、計算に SSE ベクトルのみを使用し、非 SSE ベクトルとの間で変換するためのインターフェイスを作成するだけで、それが計算の周辺機器で使用されます (多くの場合、内部の個々のコンポーネントにアクセスする必要がないため)。長い計算)。これはglmがこの問題を処理する方法のようです。しかし、Eigenがそれをどのように処理するかはわかりません。

しかし、どのように取り組んだとしても、オペレーターのオーバーロードの利点を利用せずに SSE の組み込み関数を手作りする必要はありません。

于 2012-06-01T13:29:28.820 に答える
4

式テンプレート (プロキシ オブジェクトを使用するカスタム オペレータの実装) について学習することをお勧めします。このようにして、個々の操作の周りでパフォーマンスを低下させるロード/ストアを実行することを回避し、計算全体で一度だけ実行することができます。

于 2012-06-01T13:36:39.283 に答える
2

厳密に制御された関数で裸のsimdコードを使用することをお勧めします。オーバーヘッドのためにプライマリベクトルの乗算に使用しないため、この関数は、DODに従って、操作する必要のあるVector3オブジェクトのリストを取得する必要があります。1つあるところにはたくさんあります。

于 2012-05-23T11:40:17.023 に答える