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 new
operator delete
std::allocator
std::get_temporary_buffer
std::return_temporary_buffer
operator new
しかし、本当の欠点は、SSE ベクトルをメンバーとして持つクラスの動的割り当ても考慮する必要があることです。これは面倒かもしれませんが、これらのクラスを派生させて特殊化aligned_storage
全体を配置することで、少し自動化することができます。std::allocator
便利なマクロを台無しにします。
JamesWynn は、これらの操作が特別な重い計算ブロック (テクスチャ フィルタリングや頂点変換など) にまとめられることが多いという点を指摘していますが、一方で、これらの 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
ように必要です。u
andには何も格納しませw
んでした。これは、これ以上使用しない一時変数にすぎないためです。すべてが完全にインライン化され、最適化されています。関数は s の後に実数を使用してdot
a を返しますが、XMM レジスタを離れることなく、次の乗算の内積の結果をシームレスにシャッフルすることさえできました。float
_mm_store_ss
haddps
したがって、通常はコンパイラの能力に少し懐疑的である私でさえ、独自の組み込み関数を特別な関数に手作りすることは、カプセル化によって得られるクリーンで表現力豊かなコードと比較して、実際には割に合わないと言わざるを得ません。イントリンをハンドラフティングすることで実際にいくつかの命令を省くことができるキラーな例を作成できるかもしれませんが、その場合も最初にオプティマイザの裏をかく必要があります。
編集:わかりました、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 の組み込み関数を手作りする必要はありません。