比較命令の高速化

#define SAMPLE_NUM 576
int xr_max( float xr[SAMPLE_NUM]){
    int i;
    float x, max = 0.0;

    for ( i = 0; i < SAMPLE_NUM; i++ ){
        x = fabs(xr[i]);
        if( x > max )
            max = x;
    }
    return (int)max;
}
を最適化します. (午後のこ〜だのソースの一部分です)
VC++6.0でのコンパイルは次のようになりました.
_xr_max:
_x$ = 8
_xr$ = 8
    fld     DWORD PTR _0
    mov     ecx, DWORD PTR _xr$[esp-4]
    mov     edx, 576
$L2050:
    fld     DWORD PTR [ecx]
    fabs
    fst     DWORD PTR _x$[esp-4]

    fcomp   ST(1)
    fnstsw  ax
    test    ah, 41h
    jne     SHORT $L2051
    fstp    ST(0)

    fld     DWORD PTR _x$[esp-4]
$L2051:
    add     ecx, 4
    dec     edx
    jne     SHORT $L2050

    call    __ftol
    ret
私のマシンではこれを約13000clkで実行します (当然この値はCPUやメモリ等によって大幅に変わるので飽くまで目安です). 比較の部分でtest ah,41hが使われていますが, ペアリングなどの関係でand ah,41hにすべきだそうです.
test eax,4100hも考えられますが, K6では確か遅くなったような気がします.

では fcompの代わりに整数演算命令を使ってみます.
    push    ebx
_P=4*1
    mov    ebx,[esp+_P+4]
    mov    ecx,576
    xor    eax,eax
xrm_lp:
        mov     edx,[ebx]
    add    ebx,4
        and     edx,7FFFFFFFh
    cmp    edx,eax
    jb    short xrm_F0
    mov    eax,edx
xrm_F0:
    dec    ecx
    jnz    short xrm_lp
    push    eax
    fld    dword ptr [esp]    ;(1)
        fsub    dword ptr [_05] ;=0.5
    fistp    dword ptr [esp]    ;(2)
    pop    eax        ;(3)
    pop    ebx
    ret
7FFFFFFFhとandを取ることで最上位bitがクリアされ浮動小数として正の値になります. (1)〜(3)でeaxに格納されている浮動小数の値を整数に変換します. fistpによる整数への変換方法はFPUのコントロールレジスタの値によって代わりますが, 四捨五入モードの場合は0.5を引いておけば int へのキャスト(切捨て)とほぼ同じ動作をします. この場合だと約2500clkで動作しました. 4〜5倍の高速化です. FPUの比較命令がいかに遅いかが分かります.

補足
新井 俊一さんから, 最大値 / 最小値を求める部分では分岐予測が外れることが多いので分岐しない方法 [後日, 説明予定] を使った方がよいのでは?とのメールを頂きました. 確かに一般の場合はその通りです. ただこのルーチンを実際に使っていた部分では, xr[]はほぼ単調減少することが分かっていたので分岐予測がかなりの確率でヒットするだろうと推測していました. そのため単純な分岐を採用しました. とは言え, 何も説明せずにいたのは私のミスです. 申し訳ありません.

次に3D Now!命令を使ってみます.
    femms
    mov    edx,[esp+4]
    mov    ecx,576/4
    movq    mm3,[forAbs3DN]    ;mm3=[7FFFFFFF:7FFFFFFF]
    pfsub    mm0,mm0        ;mm0=[0:0]
    movq    mm1,mm3
xr3_lp:
    pand    mm1,[edx]    ;mm1=[|xr[i+1]|:|xr[i]|]
    movq    mm2,mm3        ;mm2=for abs
    pand    mm2,[edx+8]    ;mm2=[|xr[i+3]|:|xr[i+2]|]
    add    edx,16
    pfmax    mm0,mm1        ;mm0=max(mm0,mm1)
    movq    mm1,mm3        ;mm1=for abs
    pfmax    mm0,mm2        ;mm0=max(mm0,mm2)
    loop    xr3_lp

    pf2id    mm0,mm0
    movd    eax,mm0
    psrlq    mm0,32
    movd    ecx,mm0
    cmp    eax,ecx
    jae    short xr3_exit
    mov    eax,ecx
xr3_exit:
    femms
    ret
pfmaxという命令がありますから, それを使います. 絶対値を求めるところで, 値を読み込んでからandをするよりは, 先にレジスタに7FFFFFFFhを入れておいてそれとメモリとのandを取るほうが依存関係が弱くなるのでこうしました. なお, pand命令どうしはペアリングできません. この場合約1000clkで動作します. 標準の方法に比べて10倍以上も速いです. 最後の上位32bitと下位32bitの大きい方を取る部分が少しもたついてます. うまいやり方があったら教えてください.

御意見は光成滋生<herumi@nifty.com>までお願いします