大きな整数配列の各要素に対して実行されるC++で記述されたループがあります。ループ内で、整数のいくつかのビットをマスクしてから、最小値と最大値を見つけます。これらの操作にSSE命令を使用すると、ビット単位のANDおよびif-else条件を使用して記述された通常のループと比較して、はるかに高速に実行されると聞きました。私の質問は、これらの=に進むべきかどうかです。 SSE命令?また、コードが別のプロセッサで実行された場合はどうなりますか?それでも機能しますか、またはこれらの命令はプロセッサ固有ですか?
SSEが例であるSIMDを使用すると、データの複数のチャンクに対して同じ操作を実行できます。したがって、整数演算の直接の代わりとしてSSEを使用する利点はありません。一度に複数のデータ項目に対して操作を実行できる場合にのみ、利点が得られます。これには、メモリ内で連続しているいくつかのデータ値をロードし、必要な処理を実行してから、配列内の次の値のセットにステップすることが含まれます。
問題:
1コードパスが処理中のデータに依存している場合、SIMDの実装ははるかに困難になります。例えば:
a = array [index];
a &= mask;
a >>= shift;
if (a < somevalue)
{
a += 2;
array [index] = a;
}
++index;
sIMDとして行うのは簡単ではありません:
a1 = array [index] a2 = array [index+1] a3 = array [index+2] a4 = array [index+3]
a1 &= mask a2 &= mask a3 &= mask a4 &= mask
a1 >>= shift a2 >>= shift a3 >>= shift a4 >>= shift
if (a1<somevalue) if (a2<somevalue) if (a3<somevalue) if (a4<somevalue)
// help! can't conditionally perform this on each column, all columns must do the same thing
index += 4
2データが連続していない場合、SIMD命令にデータをロードするのは面倒です
3コードはプロセッサ固有です。 SSEはIA32(Intel/AMD)のみであり、すべてのIA32CPUがSSEをサポートしているわけではありません。
アルゴリズムとデータを分析して、SSEが可能かどうかを確認する必要があります。そのためには、SSEがどのように機能するかを知る必要があります。 IntelのWebサイトにはたくさんのドキュメントがあります。
この種の問題は、優れた低レベルプロファイラーが不可欠な場合の完璧な例です。 (VTuneのようなもの)それはあなたのホットスポットがどこにあるかについてあなたにもっと多くの情報に基づいた考えを与えることができます。
私の推測では、あなたが説明していることから、あなたのホットスポットはおそらくif/elseを使用した最小/最大計算に起因する分岐予測の失敗になるでしょう。したがって、SIMD組み込み関数を使用すると、最小/最大命令を使用できるようになりますが、代わりにブランチレスの最小/最大計算を使用することをお勧めします。これにより、ほとんどの利益をより少ない痛みで達成できる可能性があります。
このようなもの:
inline int
minimum(int a, int b)
{
int mask = (a - b) >> 31;
return ((a & mask) | (b & ~mask));
}
SSE命令を使用する場合、明らかにこれらをサポートするプロセッサに制限されます。つまり、Pentium 2程度にさかのぼるx86を意味します(いつ導入されたか正確には思い出せませんが、ずっと前です)
私が思い出す限り、整数演算を提供するSSE2は、やや最近のものです(Pentium 3?最初のAMD Athlonプロセッサはそれらをサポートしていませんでしたが)
いずれの場合も、これらの手順を使用するには2つのオプションがあります。コードのブロック全体をアセンブリで記述します(おそらく悪い考えです。コンパイラがコードを最適化することは事実上不可能であり、人間が効率的なアセンブラを記述することは非常に困難です)。
または、コンパイラで使用可能な組み込み関数を使用します(メモリが機能する場合、通常はxmmintrin.hで定義されます)
ただし、パフォーマンスが向上しない場合があります。 SSEコードは、処理するデータに追加の要件を課します。主に、データは128ビット境界で整列する必要があることに注意してください。また、同じレジスタにロードされた値(128ビットSSEレジスタは4つのintを保持できます。最初と2番目のレジスタを一緒に追加することは最適ではありません。しかし、4つのintすべてを別の対応する4つのintに追加します。登録は高速になります)
すべての低レベルのSSEいじりをラップするライブラリを使用したくなるかもしれませんが、それは潜在的なパフォーマンス上の利点を台無しにする可能性もあります。
SSEの整数演算サポートがどれほど優れているかはわかりません。そのため、パフォーマンスを制限する要因になる可能性もあります。 SSEは、主に浮動小数点演算の高速化を目的としています。
Microsoft Visual C++を使用する場合は、次の内容をお読みください。
私の経験から、SSEは、コードのプレーンcバージョン(インラインasmなし、組み込み関数を使用しない)よりも大幅に(4倍以上)高速化されますが、手動で最適化されたアセンブラーは勝てることがわかりますコンパイラがプログラマの意図を理解できない場合のコンパイラ生成アセンブリ(信じてください、コンパイラはすべての可能なコードの組み合わせをカバーしているわけではなく、決してカバーしません)ああ、コンパイラは実行するデータを毎回レイアウトすることはできません可能な限り最速の速度で。ただし、Intelコンパイラよりも高速化するには、多くの経験が必要です(可能な場合)。
SSEで、あなたが説明したものと似ていますが、バイト配列にいくつかの画像処理コードを実装しました。 Intelコンパイラに関しても、正確なアルゴリズムによっては4倍以上になりますが、Cコードと比較して大幅に高速化されます。ただし、すでに述べたように、次の欠点があります。
移植性。コードはすべてのIntelのようなCPUで実行されるため、AMDでも実行されますが、他のCPUでは実行されません。ターゲットハードウェアを制御するため、これは問題ではありません。コンパイラを切り替えたり、64ビットOSに切り替えたりすることも問題になる可能性があります。
あなたは急な学習曲線を持っていますが、原則を理解した後、新しいアルゴリズムを書くことはそれほど難しくないことがわかりました。
保守性。ほとんどのCまたはC++プログラマーは、アセンブリ/ SSEの知識がありません。
パフォーマンスの向上が本当に必要で、Intel IPPのようなライブラリで問題の関数を見つけることができず、移植性の問題に対処できる場合にのみ、それを実行することをお勧めします。
コンパイラが何をしているかを理解するのに役立つコードを記述します。 GCCは、次のようなSSEコードを理解して最適化します:
typedef union Vector4f
{
// Easy constructor, defaulted to black/0 vector
Vector4f(float a = 0, float b = 0, float c = 0, float d = 1.0f):
X(a), Y(b), Z(c), W(d) { }
// Cast operator, for []
inline operator float* ()
{
return (float*)this;
}
// Const ast operator, for const []
inline operator const float* () const
{
return (const float*)this;
}
// ---------------------------------------- //
inline Vector4f operator += (const Vector4f &v)
{
for(int i=0; i<4; ++i)
(*this)[i] += v[i];
return *this;
}
inline Vector4f operator += (float t)
{
for(int i=0; i<4; ++i)
(*this)[i] += t;
return *this;
}
// Vertex / Vector
// Lower case xyzw components
struct {
float x, y, z;
float w;
};
// Upper case XYZW components
struct {
float X, Y, Z;
float W;
};
};
ビルドパラメータに-msse-msse2を含めることを忘れないでください!
SSE命令は元々Intelチップ上にありましたが、最近(Athlon以降?)AMDもそれらをサポートしているため、SSE命令セットに対してコードを実行する場合は、ほとんどのx86プロシージャに移植できるはずです。 。
そうは言っても、x86のアセンブラに既に精通していない限り、SSEコーディングを学ぶのは時間の価値がないかもしれません-より簡単なオプションは、コンパイラのドキュメントをチェックして、あるかどうかを確認することです。コンパイラーが自動生成できるようにするオプションSSEコード。一部のコンパイラーは、この方法でループを非常にうまくベクトル化します(Intelコンパイラーが次のことをうまく行っていると聞いても驚くことはないでしょう。この :)
SSEは一部のプロセッサに固有であるのは事実ですが(SSEは比較的安全かもしれませんが、私の経験ではSSE2ははるかに少ないです)、実行時にCPUを検出し、それに応じてコードを動的にロードできます。ターゲットCPU。
以前のポスターに同意します。メリットは非常に大きい場合がありますが、それを取得するには多くの作業が必要になる場合があります。これらの手順に関するIntelのドキュメントは4Kページを超えています。 Ocali Incから無料でEasySSE(組み込み関数+例のc ++ラッパーライブラリ)をチェックすることをお勧めします。
このEasySSEとの関係は明らかだと思います。
SIMD組み込み関数(SSE2など)は、この種の処理を高速化できますが、正しく使用するには専門知識が必要です。それらは、アライメントとパイプラインの待ち時間に非常に敏感です。不注意に使用すると、パフォーマンスが低下する可能性があります。キャッシュプリフェッチを使用するだけで、すべてのintがL1にあることを確認して操作できるようにすることで、はるかに簡単で迅速なスピードアップが得られます。
関数が1秒あたり100,000,000整数を超えるスループットを必要としない限り、SIMDはおそらく問題を起こす価値がありません。
異なるCPUで利用可能な異なるSSEバージョンについて以前に言われたことを簡単に追加するだけです:これは、CPUID命令によって返されるそれぞれの機能フラグを調べることで確認できます(たとえば、Intelのドキュメントを参照)詳細については)。
C/C++用のインラインアセンブラを見てください。ここに DDJの記事 があります。プログラムが互換性のあるプラットフォームで実行されることが100%確実でない限り、ここで多くの推奨事項に従う必要があります。
アセンブリにかなり熟練していない限り、これを自分で行うことはお勧めしません。 Skizz が指摘しているように、SSEを使用すると、おそらくデータを注意深く再編成する必要があり、その利点はせいぜい疑わしいことがよくあります。
非常に小さなループを記述し、データを非常に緊密に編成し、コンパイラーに依存してこれを実行する方がはるかに良いでしょう。 Intel CコンパイラとGCC(4.1以降)はどちらもコードを自動ベクトル化でき、おそらくあなたよりも優れた仕事をするでしょう。 (CXXFLAGSに-ftree-vectorizeを追加するだけです。)
編集:もう1つ言及しなければならないのは、いくつかのコンパイラがアセンブリ組み込み関数をサポートしていることです。これはおそらくIMOです。 、asm()または__asm {}構文よりも使いやすいです。