22

内部ループを持つ単純な関数があります。入力値をスケーリングし、ルックアップ テーブルで出力値を検索し、それを宛先にコピーします。(ftol_ambient は、float から int への高速変換のために Web からコピーしたトリックです)。

for (i = 0;  i < iCount;  ++i)
{
    iScaled = ftol_ambient(*pSource * PRECISION3);
    if (iScaled <= 0)
        *pDestination = 0;
    else if (iScaled >= PRECISION3)
        *pDestination = 255;
    else
    {
        iSRGB = FloatToSRGBTable3[iScaled];
        *pDestination = iSRGB;
    }
    pSource++;
    pDestination++;
}

現在、私のルックアップ テーブルは有限であり、フロートは無限であるため、オフ バイ ワン エラーが発生する可能性があります。その場合を処理するためのコードを使用して、関数のコピーを作成しました。唯一の違いは 2 行のコードが追加されていることに注意してください。見苦しいポインターのキャストは無視してください。

for (i = 0;  i < iCount;  ++i)
{
    iScaled = ftol_ambient(*pSource * PRECISION3);
    if (iScaled <= 0)
        *pDestination = 0;
    else if (iScaled >= PRECISION3)
        *pDestination = 255;
    else
    {
        iSRGB = FloatToSRGBTable3[iScaled];
        if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))
            ++iSRGB;
        *pDestination = (unsigned char) iSRGB;
    }
    pSource++;
    pDestination++;
}

ここが奇妙な部分です。100000 要素の同一の入力を 100 回繰り返して、両方のバージョンをテストしています。私の Athlon 64 1.8 GHz (32 ビット モード) では、最初の関数に 0.231 秒かかり、2 番目の (長い) 関数に 0.185 秒かかります。両方の関数が同じソース ファイル内で隣接しているため、コンパイラ設定が異なる可能性はありません。テストを実行する順序を逆にして何度もテストを実行しましたが、タイミングは毎回ほぼ同じです。

最新のプロセッサには多くの謎があることは知っていますが、どうしてこれが可能になるのでしょうか?

比較のために、Microsoft VC++6 コンパイラからの関連するアセンブラ出力を次に示します。


; 173  :    for (i = 0;  i < iCount;  ++i)

$L4455:

; 174  :    {
; 175  :        iScaled = ftol_ambient(*pSource * PRECISION3);

    fld DWORD PTR [esi]
    fmul    DWORD PTR __real@4@400b8000000000000000
    fstp    QWORD PTR $T5011[ebp]

; 170  :    int i;
; 171  :    int iScaled;
; 172  :    unsigned int iSRGB;

    fld QWORD PTR $T5011[ebp]

; 173  :    for (i = 0;  i < iCount;  ++i)

    fistp   DWORD PTR _i$5009[ebp]

; 176  :        if (iScaled <= 0)

    mov edx, DWORD PTR _i$5009[ebp]
    test    edx, edx
    jg  SHORT $L4458

; 177  :            *pDestination = 0;

    mov BYTE PTR [ecx], 0

; 178  :        else if (iScaled >= PRECISION3)

    jmp SHORT $L4461
$L4458:
    cmp edx, 4096               ; 00001000H
    jl  SHORT $L4460

; 179  :            *pDestination = 255;

    mov BYTE PTR [ecx], 255         ; 000000ffH

; 180  :        else

    jmp SHORT $L4461
$L4460:

; 181  :        {
; 182  :            iSRGB = FloatToSRGBTable3[iScaled];
; 183  :            *pDestination = (unsigned char) iSRGB;

    mov dl, BYTE PTR _FloatToSRGBTable3[edx]
    mov BYTE PTR [ecx], dl
$L4461:

; 184  :        }
; 185  :        pSource++;

    add esi, 4

; 186  :        pDestination++;

    inc ecx
    dec edi
    jne SHORT $L4455

$L4472:

; 199  :    {
; 200  :        iScaled = ftol_ambient(*pSource * PRECISION3);

    fld DWORD PTR [esi]
    fmul    DWORD PTR __real@4@400b8000000000000000
    fstp    QWORD PTR $T4865[ebp]

; 195  :    int i;
; 196  :    int iScaled;
; 197  :    unsigned int iSRGB;

    fld QWORD PTR $T4865[ebp]

; 198  :    for (i = 0;  i < iCount;  ++i)

    fistp   DWORD PTR _i$4863[ebp]

; 201  :        if (iScaled <= 0)

    mov edx, DWORD PTR _i$4863[ebp]
    test    edx, edx
    jg  SHORT $L4475

; 202  :            *pDestination = 0;

    mov BYTE PTR [edi], 0

; 203  :        else if (iScaled >= PRECISION3)

    jmp SHORT $L4478
$L4475:
    cmp edx, 4096               ; 00001000H
    jl  SHORT $L4477

; 204  :            *pDestination = 255;

    mov BYTE PTR [edi], 255         ; 000000ffH

; 205  :        else

    jmp SHORT $L4478
$L4477:

; 206  :        {
; 207  :            iSRGB = FloatToSRGBTable3[iScaled];

    xor ecx, ecx
    mov cl, BYTE PTR _FloatToSRGBTable3[edx]

; 208  :            if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))

    mov edx, DWORD PTR _SRGBCeiling[ecx*4]
    cmp edx, DWORD PTR [esi]
    jg  SHORT $L4481

; 209  :                ++iSRGB;

    inc ecx
$L4481:

; 210  :            *pDestination = (unsigned char) iSRGB;

    mov BYTE PTR [edi], cl
$L4478:

; 211  :        }
; 212  :        pSource++;

    add esi, 4

; 213  :        pDestination++;

    inc edi
    dec eax
    jne SHORT $L4472


編集: Nils Pipenbrinck の仮説をテストしようとして、最初の関数のループの前と内側に数行追加しました。

int one = 1;
int two = 2;

        if (one == two)
            ++iSRGB;

最初の関数の実行時間は 0.152 秒に短縮されました。面白い。


編集 2: Nils は、比較はリリース ビルドから最適化されると指摘しましたが、実際にそうです。アセンブリ コードの変更は非常に微妙です。ここに投稿して、手掛かりが得られるかどうかを確認します。この時点で、コードの配置であるかどうか疑問に思っていますか?

; 175  :    for (i = 0;  i < iCount;  ++i)

$L4457:

; 176  :    {
; 177  :        iScaled = ftol_ambient(*pSource * PRECISION3);

    fld DWORD PTR [edi]
    fmul    DWORD PTR __real@4@400b8000000000000000
    fstp    QWORD PTR $T5014[ebp]

; 170  :    int i;
; 171  :    int iScaled;
; 172  :    int one = 1;

    fld QWORD PTR $T5014[ebp]

; 173  :    int two = 2;

    fistp   DWORD PTR _i$5012[ebp]

; 178  :        if (iScaled <= 0)

    mov esi, DWORD PTR _i$5012[ebp]
    test    esi, esi
    jg  SHORT $L4460

; 179  :            *pDestination = 0;

    mov BYTE PTR [edx], 0

; 180  :        else if (iScaled >= PRECISION3)

    jmp SHORT $L4463
$L4460:
    cmp esi, 4096               ; 00001000H
    jl  SHORT $L4462

; 181  :            *pDestination = 255;

    mov BYTE PTR [edx], 255         ; 000000ffH

; 182  :        else

    jmp SHORT $L4463
$L4462:

; 183  :        {
; 184  :            iSRGB = FloatToSRGBTable3[iScaled];

    xor ecx, ecx
    mov cl, BYTE PTR _FloatToSRGBTable3[esi]

; 185  :            if (one == two)
; 186  :                ++iSRGB;
; 187  :            *pDestination = (unsigned char) iSRGB;

    mov BYTE PTR [edx], cl
$L4463:

; 188  :        }
; 189  :        pSource++;

    add edi, 4

; 190  :        pDestination++;

    inc edx
    dec eax
    jne SHORT $L4457
4

5 に答える 5

11

私の推測では、最初のケースでは、2つの異なるブランチがCPUの同じ分岐予測スロットに配置されます。これらの2つのブランチが、コードが遅くなるたびに異なる予測を行う場合。

2番目のループでは、追加されたコードは、分岐の1つを別の分岐予測スロットに移動するのに十分な場合があります。

インテルVTuneアナライザーまたはAMDCodeAnalystツールを試してみてください。これらのツールは、コードで何が起こっているかを正確に示します。

ただし、このコードをさらに最適化することはおそらく価値がないことに注意してください。CPUでコードを高速に調整すると、同時に別のブランドでは低速になる可能性があります。


編集:

分岐予測を読みたい場合は、Agner Fogの優れたWebサイトを試してみてください:http ://www.agner.org/optimize/

このPDFは、分岐予測スロットの割り当てについて詳しく説明しています:http ://www.agner.org/optimize/microarchitecture.pdf

于 2009-03-27T03:20:41.280 に答える
4

私の最初の推測は、2 番目のケースの方が分岐がより適切に予測されているということです。おそらく、ネストされた if が、プロセッサがより多くの情報を使用して推測するアルゴリズムを提供するためです。好奇心から、行を削除するとどうなりますか

if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))

?

于 2009-03-27T02:47:37.693 に答える
1

これらのルーチンのタイミングはどのようになっていますか?ページングまたはキャッシングがタイミングに影響を与えているのだろうか?最初のルーチンを呼び出すと、メモリにロードされるか、ページの境界を越えるか、スタックが無効なページに入る(ページインが発生する)可能性がありますが、代償を払うのは最初のルーチンだけです。

仮想メモリとキャッシングの影響を減らすために、測定を行う呼び出しを行う前に、両方の関数を1回実行することをお勧めします。

于 2009-03-27T03:50:22.160 に答える
0

この内側のループをテストしているだけですか、それとも非公開の外側のループもテストしていますか?もしそうなら、これらの3行を見てください。

if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))  
    ++iSRGB;
*pDestination = (unsigned char) iSRGB;

*pDestinationこれで、外側のループのカウンターのように見えます。したがって、値の追加の増分を実行することiSRGBで、外側のループの反復の一部をスキップして、コードが実行する必要のある作業の総量を減らすことができます。

于 2009-03-27T03:15:10.123 に答える
0

私はかつて似たような状況にありました。高速化するためにいくつかのコードをループから取り出しましたが、遅くなりました。紛らわしい。結局のところ、ループの平均回数は 1 未満でした。

教訓 (明らかに必要ありません) は、実際に実行速度を測定しない限り、コードを変更しても速度は向上しないということです。

于 2009-10-29T05:05:02.853 に答える