« x86入門(3) 関数の呼び出し規約 | メイン | x86カルトクイズ(解答編) »

x86カルトクイズ

x86の解説をいざ始めてみると,どうもblogという媒体はやりにくいので別ページで進めることにしました.すいません.まとめ直すまでしばらくお待ちください.あと基本的なことばかり続いたので,ちょっとマニアックネタに走ってみます.

というわけで突然ですがクイズです.そこそこ高い難易度に設定したつもりですが,いかがでしょう.初心者の方は全然分からなくても大丈夫です.あえて曖昧な記述をしている部分もあります.後半の答えは凄いものがあるといいなあ.あと,難問奇問募集中.

以下は断りがない限り,

  • 環境は32bit OS上のPentium4以降のx86 CPU
  • 関数の呼び出し規約は__cdecl
  • 配列は16byte alignmentされていて複数の配列はオーバーラップしていない
  • ループは4の倍数と仮定してよい

ものとします.CPUに依存する場合は明記してください.

Q1(5点)

符号なしeaxの値を45倍せよ.

Q2(6点)

次はgcc 4.2.0が出力した整数の絶対値を取得するコードである.全体のバイト長を短くせよ.
; input :  eax
; output:  eax
; destroy: edx
 
mov    edx, eax
sar    edx, 31
xor    eax, edx
sub    eax, edx

Q3(各4点)

次のコード片について高速化の余地があれば改良せよ.
/* 3.1 */
.label:
...
shr   eax, 2
jnz    .label
/* 3.2 */
.label:
...
sub   ecx, byte 1
jnz    .label

*すいません,jzはjnzのtypoでした.

Q4(8点)

mov ecx, [eax*2]とmov ecx, [eax + eax]の違いは何か(hint:NASMの出力を見ても分からない).

Q5(8点)

次のコードを実行したときのxm0の値は何か.答えは最下位32bitのみでよい.
magic dd 0x01800000
x dd 1.5 ; float
 
movss    xm0, [x]
paddd    xm0, [magic]

Q6(12点)

Q5についてxを入力ととらえたとき,通常の方法とこの方法を使った場合の利点と注意点を述べよ.

Q7(10点)

int getBit(unsigned int x)
{
    for (int i = 31; i >= 0; i--) {
        if (x & (1 << i)) return i;
    }
    return 0;
}

をbsrを使って最適化せよ.

Q8(8点)

Windows上で以下のコードは潜在的なバグがある.どこか.なお,espの値は十分大きいものとする.
sub    esp, 8192
mov    [esp], eax
add    esp, 8192

Q9(15点)

以下のコードのボトルネックはどこになると考えられるか.また高速化せよ.
/*
    -1 <= in[i] <= 1とする.
*/
void func(int *out, const float *in, int n)
{
    for (int i = 0; i < n; i++) {
        out[i] = int(in[i] * 32767);
    }
}

Q10(20点)

以下のコードを高速化せよ.ただしsrc[i] > 0 for all iとしてよい.
void log2(int *dest, const int *src, int n)
{
    for (int i = 0; i < n; i++) {
#if 0
          /* わずかな誤差で意図しない値になることがあったので修正 */
//        dest[i] = (int)(log(src[i]) / log(2));
#else
        int j;
        int x = src[i];
        for (j = -1; x > 0; j++) {
            x >>= 1;
        }
        dest[i] = j;
#endif
    }
}

コメント (5)

dsk:

全然自信なし。アセンブリ分からず。

A1.
lea eax, eax + eax*4
lea eax, eax + eax*8

A2. 短くなっていないような気がするが...
move edx, eax
sar edx, 30
inc edx
imul edx

A3. jnzじゃないんですよね...
1.
.label
...
shr eax
shr eax
jz .label

2. 遅くなるかも
.label
...
loop .after
jmp .label
.after

A4.
前者はそのままじゃムリ。後者に変換される。

A5.
0x41400000

1.5 = 0[011 1111 1]10000...000
= 3fc0 0000
0180 0000
4140 0000

A6. 利点:高速性、
注意点:指数が大きすぎる場合にINFではなく負数になることがある。

A7.
int getBit(unsigned int x) {
__asm {
bsr eax, x
}
}

A8. 分かりません。8Kくらいのスタックはとれますよね....。

A9. float→intへのキャストですよね。
桁をそろえればOK?
-32767〜32767の範囲なら1<<23+1<<16を浮動小数で加えれば、
0[100 1011 0]000 000y xxxx xxxx xxxx xxxx
で、xxxx〜xxxxのところに符号付き整数が来る。

void func(int *out, const float *in, int n) {
for (int i = 0; i < n; i++) {
float x = in[i]*32767 + (1<<23 + 1<<16);
int z;
__asm {
move eax, x
cwde
move z, eax
}
out[i] = z;
}
}

A10. iはローカル変数で良いですよね。

void func(int *dest, const int *src, int n) {
for (int i = 0; i < n; i++) {
__asm {
bsf eax, *src
mov *dest, eax
}
++dest; ++src;
}
}

Jagoon:

Q2(VC2005の逆汗コピペ)
cdq
xor eax,edx
sub eax,edx

Q8
8KBはWindowsのページサイズ(4K)を超えているので、マルチスレッド環境でスタックオーバーフローを正しく検出出来ないことがあるため。4KBごとに分けてアクセスするのが正解。

sub esp, 4096
mov [esp], eax ; 空読み
sub esp, 4096
mov [esp], eax
add esp, 8192

へるみ:

>-32767〜32767の範囲なら1<<23+1<<16を浮動小数で加えれば、
>0[100 1011 0]000 000y xxxx xxxx xxxx xxxx
>で、xxxx〜xxxxのところに符号付き整数が来る。

この方法だと四捨五入になるので[1.5, 2)の範囲が2になってしまいます.かといって(1 << 23) + (1 << 16) - 0.5を足しても銀行方式のまるめが適用されるので奇数が1小さくなってしまうんですよね.あと,負の時もずれてしまう.なかなか適用箇所が難しいです.

dsk:

なるほど。ならば(1<<22) + (1<<15)を加えてshrとか思ったけど[1.75,2)が2になりますね。doubleつかったら切り捨てが実現できそうですが、かえって遅くなりそうですね。

#全体としては予想通りボロボロですな。

neeeeeet:

A1:
lea eax, [eax+eax*4]
laa eax, [eax+eax*8]
A4:
エンコーディングが違う。
x86のアドレッシングモードはreg, base+index, base+index+disp8, base+index+disp32の4通りで
indexのみの形は基本的にないため、前者はbase+indexの特殊系で32bitのオフセットを取る形に変換され、
7バイトになる。
[0+eax*2]と書くのと同じ。
後者はbase+indexで余計なdispがつかない。
A7:
bsr eax, dword ptr[x]
jne ok
xor eax,eax
ok:
ret

A8:
スタックは自動的に伸張するが、現在割り当てられているスタック用ページの直下のページのみが
ガードページなので、一度にPage Size(=4096)より大きい量を割り当てると
Access Violationが起きる可能性がある。1ページずつタッチしていけば大丈夫。

A9:
;要sse
mov ecx, dword ptr [n]
shl ecx, 2
mov edx, dword ptr [in]
add edx, ecx
mov eax, dword ptr [out]
add eax, ecx
neg ecx

pcmpeqw xmm2, xmm2 ;xmm2 = ffff ffff ffff ffff ffff ffff ffff ffff
;psllw xmm2, 1
;pxor xmm3, xmm3
;punpckLwd xmm2,xmm3 ;xmm2 = 0000 7fff 0000 7fff 0000 7fff 0000 7fff
pslld xmm2, 17 ;xmm2 = 0000 7fff 0000 7fff 0000 7fff 0000 7fff
cvtdq2ps xmm2, xmm2
for_each_4_elements:
; unroll してここに prefetchいれたほうがいいかも?
movaps xmm0, xmm2
mulps xmm0, xmmword ptr[edx + ecx]
cvtps2dq xmm0, xmm0
movdqa xmmword ptr[eax + ecx]
add ecx, 10h
jnz for_each_4_elements

A10: sse版が思ったほど早くないので、まだまだ余地があるかも
;(1) using bsr
push esi
push edi
mov edi, dword ptr[esp+0ch] ;dest
mov esi, dword ptr[esp+10h] ;src
mov ecx, dword ptr[esp+14h] ;n
shl ecx, 2
add esi, ecx
add edi, ecx
neg ecx
for_each_elements:
bsr eax, dword ptr[esi+ecx]
jnz store
xor eax, eax
not eax
store:
mov dword ptr[edi+ecx], eax
add ecx, 4
jnz for_each_elements
pop edi
pop esi
ret
;--------------------------------------
;(2) using conversion to float
push esi
push edi
mov edi, dword ptr[esp+0ch] ;dest
mov esi, dword ptr[esp+10h] ;src
mov ecx, dword ptr[esp+14h] ;n
shl ecx, 2
add esi, ecx
add edi, ecx
neg ecx

pcmpeqd xmm7, xmm7
movdqa xmm6, xmm7
movdqa xmm5, xmm7

psrld xmm5, 31 ;xmm5 = { 00000001, 00000001, 00000001, 00000001 }
psrld xmm6, 25 ;xmm6 = { 0000007f, 0000007f, 0000007f, 0000007f }
pxor xmm7, xmm6 ;xmm7 = { ffffff80, ffffff80, ffffff80, ffffff80 }
paddd xmm7, xmm5 ;xmm7 = { ffffff81, ffffff81, ffffff81, ffffff81 }


cvtdq2ps xmm6, xmm5
pslld xmm5, 1
cvtdq2ps xmm5, xmm5
divps xmm6, xmm5 ; xmm6 = { 0.5f, 0.5f, 0.5f, 0.5f }
for_each_4_elements:
movdqa xmm0, xmmword ptr[esi+ecx]
movdqa xmm1, xmm0

cvtdq2ps xmm0, xmm0
addps xmm0, xmm6

psrld xmm0, 23 ;get exponent
paddd xmm0, xmm7 ;-126

movdqa xmmword ptr[edi+ecx], xmm0

add ecx,10h
jnz for_each_4_elements

pop edi
pop esi
ret

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)