3

O(N^4) 画像処理ループがあり、(Intel Vtune 2013 を使用して) プロファイリングした後、リタイアした命令の数が大幅に減少していることがわかります。マルチコア アーキテクチャでのこの動作を理解するのに助けが必要です。(私は Intel Xeon x5365 を使用しています - 2 つのコアごとに共有 L2 キャッシュを持つ 8 つのコアがあります)。また、分岐予想外も激増!! ///////////////EDITS/////////// 非展開コードのサンプルを以下に示します。

for(imageNo =0; imageNo<496;imageNo++){
for (unsigned int k=0; k<256; k++)
{
    double z = O_L + (double)k * R_L;
    for (unsigned int j=0; j<256; j++)
    {
        double y = O_L + (double)j * R_L;

        for (unsigned int i=0; i<256; i++)
        {
            double x[1] = {O_L + (double)i * R_L} ;             
            double w_n =  (A_n[2] * x[0] + A_n[5] * y + A_n[8] * z + A_n[11])  ;
            double u_n =  ((A_n[0] * x[0] + A_n[3] * y + A_n[6] * z + A_n[9] ) / w_n);
            double v_n =  ((A_n[1] * x[0] + A_n[4] * y + A_n[7] * z + A_n[10]) / w_n);                      

            for(int loop=0; loop<1;loop++)
            {
                px_x[loop] = (int) floor(u_n);
                px_y[loop] = (int) floor(v_n);
                alpha[loop] = u_n - px_x[loop] ;
                beta[loop]  = v_n - px_y[loop] ;
            }
///////////////////(i,j) pixels ///////////////////////////////
                if (px_x[0]>=0 && px_x[0]<(int)threadCopy[0].S_x && px_y[0]>=0 && px_y[0]<(int)threadCopy[0].S_y)                   

                    pixel_1[0] = threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + px_x[0]];
                else
                    pixel_1[0] = 0.0;               

                if (px_x[0]+1>=0 && px_x[0]+1<(int)threadCopy[0].S_x && px_y[0]>=0 && px_y[0]<(int)threadCopy[0].S_y)                   

                    pixel_1[2] = threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + (px_x[0]+1)];
                else
                    pixel_1[2] = 0.0;                   

/////////////////// (i+1, j) pixels/////////////////////////    

                if (px_x[0]>=0 && px_x[0]<(int)threadCopy[0].S_x && px_y[0]+1>=0 && px_y[0]+1<(int)threadCopy[0].S_y)
                    pixel_1[1] = threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + px_x[0]];
                else
                    pixel_1[1] = 0.0;                   

                if (px_x[0]+1>=0 && px_x[0]+1<(int)threadCopy[0].S_x && px_y[0]+1>=0 && px_y[0]+1<(int)threadCopy[0].S_y)

                    pixel_1[3] = threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + (px_x[0]+1)];

                else 

                    pixel_1[3] = 0.0;

                pix_1 = (1.0 - alpha[0]) * (1.0 - beta[0]) * pixel_1[0] + (1.0 - alpha[0]) * beta[0]  * pixel_1[1]

                +  alpha[0]  * (1.0 - beta[0]) * pixel_1[2]   +  alpha[0]  *  beta[0]  * pixel_1[3];                                

            f_L[k * L * L + j * L + i] += (float)(1.0 / (w_n * w_n) * pix_1);
                        }
    }

}
   }

最も内側のループを 4 回の反復で展開しています (ループをどのように削除したかという一般的な理想が得られます。基本的には、Array[4] の配列を作成し、それぞれの値を入力します)。反復回数の合計を 75% 削減します。すべてのループ (load i、inc i、cmp i、jle ループ) に 4 つのループ処理命令があるとします。アンロール後の命令の総数は、(256-64)*4*256*256*496=24.96G 減少するはずです。 . プロファイリングされた結果は次のとおりです。

Before UnRolling: Instr retired: 3.1603T      no of branch mis-predictions: 96 million
After UnRolling:  Instr retired: 2.642240T    no of branch mis-predictions: 144 million

引退したインストルメントは 518.06G 減少しました。これがどのように起こっているのか、私にはわかりません。これに関する助けをいただければ幸いです(たとえそれが発生する可能性が低い場合でも)。また、ブランチの予測ミスが増えている理由を知りたいです。前もって感謝します!

4

1 に答える 1

4

gcc がどこで命令数を削減するかは明らかではありません。レジスタ プレッシャーの増加により、gcc が load+operate 命令を使用するようになる可能性があります (したがって、プリミティブ操作の数は同じですが、命令は少なくなります)。のインデックスはf_L、最も内側のループごとに 1 回だけインクリメントされますが、これは 6.2G (3*64*256*256*496) 命令しか節約できません。(ちなみに、ループ オーバーヘッドはi、レジスタに残る必要があるため、3 命令のみにする必要があります。)

次の疑似アセンブリ (RISC のような ISA の場合) は、双方向展開を使用して増分を保存する方法を示しています。

// the address of f_L[k * L * L + j * L + i] is in r1
// (float)(1.0 / (w_n * w_n) * pix_1) results are in f1 and f2
load-single f9 [r1];    // load float at address in r1 to register f9
add-single f9 f9 f1;    // f9 = f9 + f1
store-single [r1] f9;   // store float in f9 to address in r1
load-single f10 4[r1];  // load float at address of r1+4 to f10
add-single f10 f10 f2;  // f10 = f10 + f2
store-single 4[r1] f10; // store float in f10 to address of r1+4
add r1 r1 #8;           // increase the address by 8 bytes

展開されていないバージョンの 2 つの反復のトレースは、次のようになります。

load-single f9 [r1];  // load float at address of r1 to f9
add-single f9 f9 f2;  // f9 = f9 + f2
store-single [r1] f9; // store float in f9 to address of r1
add r1 r1 #4;         // increase the address by 4 bytes
...
load-single f9 [r1];  // load float at address of r1 to f9
add-single f9 f9 f2;  // f9 = f9 + f2
store-single [r1] f9; // store float in f9 to address of r1
add r1 r1 #4;         // increase the address by 4 bytes

メモリ アドレス指定命令には通常、即値オフセットの追加が含まれ (Itanium は特殊な例外です)、パイプラインは通常、即値が 0 の場合を最適化するように実装されていないため、0 以外の即値オフセットを使用することは一般に「自由」です。(この場合、7 対 8 の命令数が確実に減少しますが、一般的にはパフォーマンスも向上します。)

分岐予測に関しては、Agner Fog のThe microarchitecture of Intel, AMD and VIA CPUs: An Optimization Guide for Assembly Programmer and Compiler Makers ( PDF ) によると、Core2 マイクロアーキテクチャの分岐予測子は 8 ビットのグローバル履歴を使用します。これは、最後の 8 つの分岐の結果を追跡し、これらの 8 ビット (命令アドレスからのビットと共に) を使用してテーブルのインデックスを作成することを意味します。これにより、近くのブランチ間の相関関係を認識することができます。

あなたのコードでは、たとえば 8 番目の前の分岐に対応する分岐は (短絡が使用されているため) 各反復で同じ分岐ではないため、相関がどの程度認識されるかを概念化するのは容易ではありません。

分岐におけるいくつかの相関関係は明らかです。px_x[0]>=0が true の場合、これも true になりpx_x[0]+1>=0ます。が真である場合px_x[0] <(int)threadCopy[0].S_xは、真でpx_x[0]+1 <(int)threadCopy[0].S_xある可能性が高くなります。

px_x[n]の 4 つの値すべてについてテストされるようにアンローリングが行われる場合、nこれらの相関はさらに遠ざけられ、結果が分岐予測子によって使用されなくなります。

いくつかの最適化の可能性

最適化の可能性については質問されていませんが、調査のためのいくつかの手段を提供します。

まず、ブランチについては、厳密に移植可能でなくても問題ない場合は、テストx>=0 && x<yを単純化して(unsigned)x<(unsigned)y. これは厳密には移植可能ではありません。たとえば、マシンは理論的には最上位ビットを符号とし、負をゼロ ビットで示す符号-大きさ形式で負の数を表すことができるからです。y符号付き整数の一般的な表現では、負のx値には最上位ビットが設定さyれ、符号なし整数として解釈されるよりも大きくなるため、正の符号付き整数である限り、このような再解釈キャストは機能します。

px_x次に、 または のいずれかに 100% の相関を使用することで、分岐の数を大幅に減らすことができますpx_y

if ((unsigned) px_y[0]<(unsigned int)threadCopy[0].S_y)
{
    if ((unsigned)px_x[0]<(unsigned int)threadCopy[0].S_x)
        pixel_1[0] = threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + px_x[0]];
    else
        pixel_1[0] = 0.0;
    if ((unsigned)px_x[0]+1<(unsigned int)threadCopy[0].S_x)
        pixel_1[2] = threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + (px_x[0]+1)];
    else
        pixel_1[2] = 0.0;
}
if ((unsigned)px_y[0]+1<(unsigned int)threadCopy[0].S_y)
{
    if ((unsigned)px_x[0]<(unsigned int)threadCopy[0].S_x)
        pixel_1[1] = threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + px_x[0]];
    else
        pixel_1[1] = 0.0;
    if ((unsigned)px_x[0]+1<(unsigned int)threadCopy[0].S_x)
        pixel_1[3] = threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + (px_x[0]+1)];
    else
        pixel_1[3] = 0.0;
}

(コードの上記のセクションがアンロールのために複製される場合、分岐が分岐の近くにあり、最初の分岐が他の分岐と分岐の近くにあることを可能にするために、さまざまなpx_xと のpx_y値のテストをインターリーブするのではなく、おそらくブロックとして複製する必要があります。 .)px_ypx_y+1px_xpx_xpx_x+1

別の可能な最適化はw_n、 の計算をその逆数の計算に変更することです。これにより、1 つの乗算と 3 つの除算が 4 つの乗算と 1 つの除算に変わります。除算は、乗算よりもはるかにコストがかかります。さらに、近似逆数の計算は、通常、ニュートン・ラフソン法によって改良できる開始点を提供する逆数推定命令があるため、はるかに SIMD に適しています。

コードのさらに悪い難読化が許容される場合は、コードを に変更することを検討しdouble y = O_L + (double)j * R_L;てくださいdouble y = O_L; ... y += R_L;。(テストを実行しましたが、gcc はこの最適化を認識していないようです。おそらく、浮動小数点の使用と double へのキャストが原因です。)

for(int imageNo =0; imageNo<496;imageNo++){

double z = O_L;
for (unsigned int k=0; k<256; k++)
{

    double y = O_L;
    for (unsigned int j=0; j<256; j++)
    {
        double x[1]; x[0] = O_L;
        for (unsigned int i=0; i<256; i++)
        {
            ...
            x[0] +=  R_L ;
        } // end of i loop
        y += R_L;
    }  // end of j loop
    z += R_L;
} // end of k loop
    } // end of imageNo loop

これによりパフォーマンスがわずかに向上すると推測しているため、難読化のコストは利点に比べて高くなります。

試してみる価値があるかもしれないもう 1 つの変更は、pix_1条件付きで値を設定するセクションに計算の一部を組み込むことpixel_1[]です。これにより、コードが大幅に難読化され、あまりメリットがない可能性があります。さらに、コンパイラによる自動ベクトル化がより困難になる可能性があります。(条件付きで値を適切I_nまたはゼロに設定すると、SIMD 比較で各要素が -1 または 0 に設定され、単純なandI_nで正しい値が提供されます。この場合、I_nベクトルを形成するオーバーヘッドはおそらくCore2 が 2 幅の倍精度 SIMD のみをサポートしていることを考えると価値がありますが、ギャザーのサポートまたはより長いベクトルでさえ、トレードオフが変わる可能性があります。)

ただし、この変更により、基本ブロックのサイズpx_x大きくなり、とのいずれかが範囲外の場合の計算量が削減さpx_yれます (これは一般的ではないと推測しているため、メリットはせいぜいごくわずかです)。

double pix_1 = 0.0;
double alpha_diff = 1.0 - alpha;
if ((unsigned) px_y[0]<(unsigned int)threadCopy[0].S_y)
{
    double beta_diff = 1.0 - beta;
    if ((unsigned)px_x[0]<(unsigned int)threadCopy[0].S_x)
        pix1 += alpha_diff * beta_diff 
             * threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + px_x[0]];
    // no need for else statement since pix1 is already zeroed and not 
    // adding the pixel_1[0] factor is the same as zeroing pixel_1[0]
    if ((unsigned)px_x[0]+1<(unsigned int)threadCopy[0].S_x)
        pix1 += alpha * beta_diff 
             * threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + (px_x[0]+1)];
}
if ((unsigned)px_y[0]+1<(unsigned int)threadCopy[0].S_y)
{
    if ((unsigned)px_x[0]<(unsigned int)threadCopy[0].S_x)
        pix1 += alpha_diff * beta 
             * threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + px_x[0]];
    if ((unsigned)px_x[0]+1<(unsigned int)threadCopy[0].S_x)
        pix1 += alpha * beta 
             * threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + (px_x[0]+1)];
}

理想的には、あなたのようなコードはベクトル化されますが、gcc に機会を認識させる方法、組み込み関数を使用して機会を表現する方法、またはこのコードを手動でベクトル化する多大な努力が SIMD 幅がわずか 2 の場合に価値があるかどうかはわかりません.

私はプログラマーではなく (コンピュータ アーキテクチャについて学び、考えるのが好きなだけの人間です)、マイクロ最適化に傾倒しています (上記から明らかなように)。

于 2014-03-20T02:57:20.563 に答える