次はメモリコピー関数memcpy()を作ってみます.
char *dest;
char *src;
// -------------
int i;
for (i = 0; i < n; i++) {
*dest++ = *src++;
}
これをASMで書いてみます. eaxにdest, edxにsrc, ecxにn, blに*srcの値を入れることにします.
%assign _P (4*1) ;pushした数
;memcpy(void *dest, const void *src, size_t n);
proc mymemcpy
push ebx
mov eax, [esp+_P+4]
mov edx, [esp+_P+8]
mov ecx, [esp+_P+12]
.lp:
mov bl, [edx]
inc edx
mov [eax], bl
inc eax
dec ecx
jnz .lp
pop ebx
ret
%assign _P 4*1
Cでの#define _P 4*1に相当します. こうしておくとpushした数が変わった時に対処し易くなります(私は, ですが).
push ebx
ebxの下位8bitのblを使うので退避しておきます.
inc eax
inc edx
eax, edxの値を1増やします (INCrement by 1). 1減らすならdecです (DECrement by 1).
dec ecx
jnz .lp
Cで書くところの
ecx--;
if (ecx != 0) goto .lp;
の意味です. jnzはもし0でないならジャンプせよという意味のJump if Non Zeroの略です.
原因はどこにあるかと言うと細切れに書き込んだことにあります. つまり, 32bitCPUというからには32bitずつアクセクした方が速いのです. 折角32bitレジスタを持っているのだからそれを使いましょう.
まずループ内を簡潔に表現します. 上でeax, edx, ecxは同じ値ずつ増減していましたが, 両方計算するのは無駄です. Cで書くなら次のようでしょうか.
int diff;
char *endp;
diff = src - dest;
endp = dest + n;
for (;dest < endp; dest += 4) {
*(int *)(dest + diff) = *(int *)dest;
}
eaxにdest, edxにsrc - dest, ecxにdest + nを入れることにします.
%assign _P 4*1 ;pushした数
;memcpy(void *dest, const void *src, size_t n);
proc mymemcpy
push ebx
mov eax, [esp+_P+4]
mov edx, [esp+_P+8]
dec edx, eax
mov ecx, [esp+_P+12]
add ecx, eax
.lp:
mov ebx, [eax+edx]
mov [eax], ebx
add eax, 4
cmp eax, ecx
jb .lp
pop ebx
ret
cmp eax,ecx
jb .lp
if (eax < ecx) goto .lp;の意味です. jbはeax - ecx < 0ならジャンプという意味のJump if Belowの略です. これで大分速くなったはずです.
ただ関数に渡された時点でのdestやsrcの値が奇数だとまたもや遅くなるのです. 32bitアクセスが速いと言っても本当は4の倍数のアドレスをアクセスする時のみ 速いのです. そうでない場合はペナルティが発生します. Cコンパイラはこれを知っていて, mallocなどで確保されたメモリの先頭アドレスは4の倍数(or 8, 16の倍数)に調節されています. しかし, ASMで自分でスタックなどからメモリを確保するときにはその点に注意しなければなりません.
面倒なので省略しますがdestが奇数でも高速に動作させるには例えばこういう風にする必要があります.
ま, 極力そうならないようにデータを注意して配置することですね.
静的なデータの場合, NASMには4の倍数のアドレスに強制的に置く命令として
align 4というのがあり, これを使います.
ただこの命令自身, ほとんど過去の遺物でして現在では使っても使わなくてもほとんど速度に影響しません.
さて,
rep movsd
これは
dest = ds:[esi];
src = es:[edi];
memcpy(dest, src, ecx*4);
という意味です. つまりmemcpyは殆ど1命令で出来るというわけでした(^^;
%assign _P 4*3 ;pushした数
;memcpy(void *dest, const void *src, size_t n);
proc mymemcpy
push es
push esi
push edi
mov esi, [esp+_P+4]
mov edi, [esp+_P+8]
mov ecx, [esp+_P+12]
shr ecx, 2
mov ax, ds
mov es, ax
rep movsd
pop edi
pop esi
pop es
ret
shr ecx, 2
ecxを2だけ論理右シフトします. 1だけ論理右シフトするとは
[a31][a30]...[a1][a0]
という数値を(a0~a31は1bitを表す:a0が一番下を表す)
[数値の0][a31][a30]...[a2][a1]
と右にずらして一番左の空いたところに0を詰める操作のことです.
1だけ論理右シフトするごとに値は2分の1 (端数は切捨て) となるので shr ecx, 2はecxを4で割るという意味です.
mov ax, ds
mov es, ax
普通は同じセグメント間の数値を複写するのでes = dsと設定します.
push ds
pop es
としてもかまいませんが, mov es, dsとは出来ません.