C ++を使用してインラインアセンブリで動作しないSSE2命令
-
10-10-2019 - |
質問
SSE2を使用していくつかの値を一緒に追加するこの関数があります。LHSとRHSを一緒に追加し、結果をLHSに戻すことになっています。
template<typename T>
void simdAdd(T *lhs,T *rhs)
{
asm volatile("movups %0,%%xmm0"::"m"(lhs));
asm volatile("movups %0,%%xmm1"::"m"(rhs));
switch(sizeof(T))
{
case sizeof(uint8_t):
asm volatile("paddb %%xmm0,%%xmm1":);
break;
case sizeof(uint16_t):
asm volatile("paddw %%xmm0,%%xmm1":);
break;
case sizeof(float):
asm volatile("addps %%xmm0,%%xmm1":);
break;
case sizeof(double):
asm volatile("addpd %%xmm0,%%xmm1":);
break;
default:
std::cout<<"error"<<std::endl;
break;
}
asm volatile("movups %%xmm0,%0":"=m"(lhs));
}
そして、私のコードは次のような関数を使用します:
float *values=new float[4];
float *values2=new float[4];
values[0]=1.0f;
values[1]=2.0f;
values[2]=3.0f;
values[3]=4.0f;
values2[0]=1.0f;
values2[1]=2.0f;
values2[2]=3.0f;
values2[3]=4.0f;
simdAdd(values,values2);
for(uint32_t count=0;count<4;count++) std::cout<<values[count]<<std::endl;
ただし、これは機能していません。コードが実行されると、2,4,6,8の代わりに1,2,3,4が出力されるため
解決
インラインアセンブリサポートは、ほとんどの最新のコンパイラでは信頼できないことがわかりました(実装は単なるバグのようです)。通常、使用する方が良いです コンパイラの内因性 C関数のように見えるが、実際には特定のオペコードにコンパイルされる宣言です。
Intrinsicsを使用すると、オペコードの正確なシーケンスを指定できますが、レジスタの着色をコンパイラに残します。 C変数とASMレジスタ間でデータを移動しようとするよりもはるかに信頼性が高く、インラインアセンブラーが常に私のために倒れています。また、コンパイラがあなたの指示をスケジュールすることができます。 パイプラインの危険. 。つまり、この場合、できる
void simdAdd(float *lhs,float *rhs)
{
_mm_storeu_ps( lhs, _mm_add_ps(_mm_loadu_ps( lhs ), _mm_loadu_ps( rhs )) );
}
とにかく、あなたの場合、あなたは2つの問題を抱えています:
- ひどいGCCインラインアセンブリ構文は、ポインターと値の違いを大きく混乱させます。使用する
*lhs
と*rhs
LHSとRHSだけの代わりに。どうやら「= M」構文は、「物そのものの代わりにあなたを追い越すこのことへのポインターを暗黙的に使用する」ことを意味します。 - GCCにはソース、宛先構文があります - ADDPSは結果を2番目のパラメーターに保存するため、出力する必要があります
xmm1
, 、 いいえxmm0
.
私は置いた CodePadの固定例 (この答えを乱雑にしないようにし、それが機能することを実証するため)。
他のヒント
ここで私が間違っていると思うものをカップル。まず、XMMレジスタをロードして値を変数に戻すステートメントは間違っています。
asm volatile("movups %0,%%xmm0"::"m"(lhs));
asm volatile("movups %0,%%xmm1"::"m"(rhs));
...
asm volatile("movups %%xmm0,%0":"=m"(lhs));
読む必要があります
asm volatile("movups %0,%%xmm0"::"m"(*lhs));
asm volatile("movups %0,%%xmm1"::"m"(*rhs));
...
asm volatile("movups %%xmm0,%0":"=m"(*lhs));
*'sに注意してください。ポインター値をロードして追加してから、ポインター引数を渡すために使用された一時的な値に戻しました(その結果、関数呼び出しが返されたときにメモリに書き込むことなく忘れられます)。
これらの修正があっても、一般的に、これは良いテクニックではありません。私は自分の例をASMステートメントで書きましたが、渡されるパラメーターの整理されていない性質を説明するのを忘れていたために欠陥がありました。ASMステートメントに関係し、本質的な機能を使用してはるかに簡単で読みやすくなります。正しいデータ型を使用するには注意してください。
template<typename T>
void simdAdd(T *lhs,T *rhs)
{
switch(sizeof(T))
{
case sizeof(uint8_t):
{
__m128i lh128;
lh128 = _mm_add_epi8( _mm_loadu_si128( (__m128i *)lhs ),
_mm_loadu_si128( (__m128i *)rhs ) );
_mm_storeu_si128( (__m128i *)lhs, lh128 );
}
break;
case sizeof(uint16_t):
{
__m128i lh128;
lh128 = _mm_add_epi16( _mm_loadu_si128( (__m128i *)lhs ),
_mm_loadu_si128( (__m128i *)rhs ) );
_mm_storeu_si128( (__m128i *)lhs, lh128 );
}
break;
case sizeof(float):
{
__m128 lh128;
lh128 = _mm_add_ps( _mm_loadu_ps( (float *)lhs ),
_mm_loadu_ps( (float *)rhs ) );
_mm_storeu_ps( (float *)lhs, lh128 );
}
break;
case sizeof(double):
{
__m128d lh128;
lh128 = _mm_add_pd( _mm_loadu_pd( (double *)lhs ),
_mm_loadu_pd( (double *)rhs ) );
_mm_storeu_pd( (double *)lhs, lh128 );
}
break;
default:
std::cout<<"error"<<std::endl;
break;
}
}
注意すべきことは、データ型のサイズで、どのデータ型に合格したかを知るのに十分ではありません。テンプレートタイプがチェックしている基本タイプと同じサイズを共有しているからといって、それが同じタイプであるという意味ではありません。だから私は私の例でこのケースをカバーするようにキャスティングを強制します。これは、この関数が指定したタイプでのみ使用されることを確信していない限り、一般に安全でない練習である可能性があります。たとえば、フロートサイズの整数を使用すると、予想外に間違った答えが得られ、コンパイラはそれについて警告することができません。