MMX IntrinsicsおよびMicrosoft C ++を使用したスタックの使用
-
04-10-2019 - |
質問
MMX命令を備えたINT32データアレイの要素を累積的に追加するインラインアセンブラーループがあります。特に、MMXレジスタが16のINT32に対応して16の異なる累積合計を並行して計算できるという事実を使用します。
このコードをMMX Intrinsicsに変換したいと思いますが、コンパイラを明示的に整理して8 MMXレジスタを使用して16の独立した金額を伴うため、パフォーマンスペナルティに陥るのではないかと心配しています。
誰かがこれについてコメントし、以下のコードを変換して内因性を使用する方法についての解決策を提案することができますか?
==インラインアセンブラー(ループ内の部分のみ)==
paddd mm0, [esi+edx+8*0] ; add first & second pair of int32 elements
paddd mm1, [esi+edx+8*1] ; add third & fourth pair of int32 elements ...
paddd mm2, [esi+edx+8*2]
paddd mm3, [esi+edx+8*3]
paddd mm4, [esi+edx+8*4]
paddd mm5, [esi+edx+8*5]
paddd mm6, [esi+edx+8*6]
paddd mm7, [esi+edx+8*7] ; add 15th & 16th pair of int32 elements
- ESIは、データ配列の開始を指します
- EDXは、現在のループ反復のデータ配列のオフセットを提供します
- データアレイは、16の独立した合計の要素がインターリーブされるように配置されています。
解決
VS2010は、Intrinsicsを使用して同等のコードで適切な最適化ジョブを実行します。ほとんどの場合、それは本質的なものをコンパイルします:
sum = _mm_add_pi32(sum, *(__m64 *) &intArray[i + offset]);
のようなものに:
movq mm0, mmword ptr [eax+8*offset]
paddd mm1, mm0
これはあなたほど簡潔ではありません padd mm1, [esi+edx+8*offset]
, 、しかし、それは間違いなくかなり近いです。実行時間は、メモリフェッチによって支配される可能性があります。
キャッチは、VSが他のMMXレジスタにのみMMXレジスタを追加することを好むように見えることです。上記のスキームは、最初の7つの合計でのみ機能します。 8番目の金額では、一部のレジスタを一時的にメモリに保存する必要があります。
これは完全なプログラムと、その対応するコンパイルされたアセンブリ(リリースビルド)です。
#include <stdio.h>
#include <stdlib.h>
#include <xmmintrin.h>
void addWithInterleavedIntrinsics(int *interleaved, int count)
{
// sum up the numbers
__m64 sum0 = _mm_setzero_si64(), sum1 = _mm_setzero_si64(),
sum2 = _mm_setzero_si64(), sum3 = _mm_setzero_si64(),
sum4 = _mm_setzero_si64(), sum5 = _mm_setzero_si64(),
sum6 = _mm_setzero_si64(), sum7 = _mm_setzero_si64();
for (int i = 0; i < 16 * count; i += 16) {
sum0 = _mm_add_pi32(sum0, *(__m64 *) &interleaved[i]);
sum1 = _mm_add_pi32(sum1, *(__m64 *) &interleaved[i + 2]);
sum2 = _mm_add_pi32(sum2, *(__m64 *) &interleaved[i + 4]);
sum3 = _mm_add_pi32(sum3, *(__m64 *) &interleaved[i + 6]);
sum4 = _mm_add_pi32(sum4, *(__m64 *) &interleaved[i + 8]);
sum5 = _mm_add_pi32(sum5, *(__m64 *) &interleaved[i + 10]);
sum6 = _mm_add_pi32(sum6, *(__m64 *) &interleaved[i + 12]);
sum7 = _mm_add_pi32(sum7, *(__m64 *) &interleaved[i + 14]);
}
// reset the MMX/floating-point state
_mm_empty();
// write out the sums; we have to do something with the sums so that
// the optimizer doesn't just decide to avoid computing them.
printf("%.8x %.8x\n", ((int *) &sum0)[0], ((int *) &sum0)[1]);
printf("%.8x %.8x\n", ((int *) &sum1)[0], ((int *) &sum1)[1]);
printf("%.8x %.8x\n", ((int *) &sum2)[0], ((int *) &sum2)[1]);
printf("%.8x %.8x\n", ((int *) &sum3)[0], ((int *) &sum3)[1]);
printf("%.8x %.8x\n", ((int *) &sum4)[0], ((int *) &sum4)[1]);
printf("%.8x %.8x\n", ((int *) &sum5)[0], ((int *) &sum5)[1]);
printf("%.8x %.8x\n", ((int *) &sum6)[0], ((int *) &sum6)[1]);
printf("%.8x %.8x\n", ((int *) &sum7)[0], ((int *) &sum7)[1]);
}
void main()
{
int count = 10000;
int *interleaved = new int[16 * count];
// create some random numbers to add up
// (note that on VS2010, RAND_MAX is just 32767)
for (int i = 0; i < 16 * count; ++i) {
interleaved[i] = rand();
}
addWithInterleavedIntrinsics(interleaved, count);
}
これは、Sumループの内側部分の生成されたアセンブリコードです(そのプロログとエピログなし)。 MM1-MM6でほとんどの合計が効率的に保持される方法に注意してください。それとは対照的に、それを各合計に追加するために数をもたらすために使用され、最後の2つの合計を提供するMM7と対照的です。このプログラムの7サムバージョンには、MM7の問題がないようです。
012D1070 movq mm7,mmword ptr [esp+18h]
012D1075 movq mm0,mmword ptr [eax-10h]
012D1079 paddd mm1,mm0
012D107C movq mm0,mmword ptr [eax-8]
012D1080 paddd mm2,mm0
012D1083 movq mm0,mmword ptr [eax]
012D1086 paddd mm3,mm0
012D1089 movq mm0,mmword ptr [eax+8]
012D108D paddd mm4,mm0
012D1090 movq mm0,mmword ptr [eax+10h]
012D1094 paddd mm5,mm0
012D1097 movq mm0,mmword ptr [eax+18h]
012D109B paddd mm6,mm0
012D109E movq mm0,mmword ptr [eax+20h]
012D10A2 paddd mm7,mm0
012D10A5 movq mmword ptr [esp+18h],mm7
012D10AA movq mm0,mmword ptr [esp+10h]
012D10AF movq mm7,mmword ptr [eax+28h]
012D10B3 add eax,40h
012D10B6 dec ecx
012D10B7 paddd mm0,mm7
012D10BA movq mmword ptr [esp+10h],mm0
012D10BF jne main+70h (12D1070h)
それで、あなたは何ができますか?
7サムおよび8サムの内因性ベースのプログラムをプロファイルします。より速く実行するものを選択します。
一度に1つのMMXレジスタのみを追加するバージョンをプロファイルします。それはまだ現代のプロセッサであるという事実を利用できるはずです 一度にキャッシュに64〜128バイトを取得します. 。 8サムバージョンのバージョンが1サムのバージョンよりも高速であることは明らかではありません。 1サムバージョンは、まったく同じ量のメモリを取得し、まったく同じ数のMMX追加を実行します。ただし、それに応じて入力をインターリーブする必要があります。
ターゲットハードウェアが許可されている場合は、使用を検討してください SSEの指示. 。これらは、一度に4つの32ビット値を追加できます。 SSEは、1999年のPentium III以来、Intel CPUで利用できます。