続・memsetを(自分も)作ってみた
DWORD単位でコピー
何人かから指摘が入ったので前回のmemsetを4バイト単位でコピーするようにしました。
#include <stdio.h> #include <string.h> #include <assert.h> __declspec( naked ) void *_memset( void *s, int c, size_t n ) { __asm { PUSH EDI ; cdeclではEDIは潰しちゃダメらしいので退避 MOV EDI, [ESP+08h] ; void *s MOV EAX, [ESP+0Ch] ; int c MOV ECX, [ESP+10h] ; size_t n ;PUSHFD ; DFフラグをいじるため保存 ;CLD ; EDIをインクリメントする MOV EDX, EAX ; DWORDのデータを作成 AND EDX, 0FFh ; 1バイト分だけ取り出す SHL EAX, 8 ; 1バイト分左シフト OR EAX, EDX ; 下位1バイトを埋める MOV DX, AX ; 下位2バイトをコピー BSWAP EAX ; 上下2バイトをスワップ MOV AX, DX ; 下位2バイトをコピー MOV EDX, ECX SHR ECX, 2 ; 4で割る REP STOSD ; REPプレフィックスを用いて連続コピー MOV ECX, EDX ; DWORD(=4バイト)でコピー AND ECX, 03h ; 4で割った端数分をコピー REP STOSB ; REPプレフィックスを用いて連続コピー ;POPFD ; DFフラグを元に戻す POP EDI ; EDIを元に戻す MOV EAX, [ESP+04h] ; return s RET } } int main() { // 確保した領域外にアクセスするため前後にもメモリ確保 int _[100] = {0}; char a[100]; int a1[100] = {0}; char b[100]; int b1[100] = {0}; memset( a, 0, sizeof( a ) ); _memset( b, 0, sizeof( b ) ); assert( !memcmp( a, b, sizeof( a ) ) ); memset( a, 42, 3 ); _memset( b, 42, 3 ); assert( !memcmp( a, b, sizeof( a ) ) ); memset( a + 12, 56892, 35 ); _memset( b + 12, 56892, 35 ); assert( !memcmp( a, b, sizeof( a ) ) ); // わざとDFをセットする __asm { STD } memset( a, 10, 10 ); _memset( b, 10, 10 ); assert( !memcmp( a, b, sizeof( a ) ) ); memset( a, -1, 0 ); _memset( b, -1, 0 ); assert( !memcmp( a, b, sizeof( a ) ) ); memset( a, -1, sizeof( a ) ); _memset( b, -1, sizeof( b ) ); assert( !memcmp( a, b, sizeof( a ) ) ); // DFをクリアしないとputsで落ちる・・・ __asm { CLD } puts( "test complete!" ); return 0; }
関数の前後でEFLAGSは保証されないのでDFがセットされていても問題なく動かなければならない・・・はずなんですが、なぜかassertに引っかかるのでわざと_memsetのCLDはコメントアウトしてあります。
環境はVS2005SP1でコンパイルオプションは何もしていせずにclコマンドでコンパイルしました。
ちなみに余計ですがPUSHFD/POPFDもコメントアウトしてありますが追加してあります。
結果
で、これで早くなるかなと以下のテストを行ないました。
#include <stdio.h> #include <time.h> #include <stdlib.h> __declspec( naked ) void *memset1( void *s, int c, size_t n ) { __asm { PUSH EDI ; cdeclではEDIは潰しちゃダメらしいので退避 MOV EDI, [ESP+08h] ; void *s MOV EAX, [ESP+0Ch] ; int c MOV ECX, [ESP+10h] ; size_t n REP STOSB ; REPプレフィックスを用いて連続コピー POP EDI ; EDIを元に戻す MOV EAX, [ESP+04h] ; return s RET } } __declspec( naked ) void *memset2( void *s, int c, size_t n ) { __asm { PUSH EDI ; cdeclではEDIは潰しちゃダメらしいので退避 MOV EDI, [ESP+08h] ; void *s MOV EAX, [ESP+0Ch] ; int c MOV ECX, [ESP+10h] ; size_t n ;PUSHFD ; DFフラグをいじるため保存 ;CLD ; EDIをインクリメントする MOV EDX, EAX ; DWORDのデータを作成 AND EDX, 0FFh ; 1バイト分だけ取り出す SHL EAX, 8 ; 1バイト分左シフト OR EAX, EDX ; 下位1バイトを埋める MOV DX, AX ; 下位2バイトをコピー BSWAP EAX ; 上下2バイトをスワップ MOV AX, DX ; 下位2バイトをコピー MOV EDX, ECX SHR ECX, 2 ; 4で割る REP STOSD ; REPプレフィックスを用いて連続コピー MOV ECX, EDX ; DWORD(=4バイト)でコピー AND ECX, 03h ; 4で割った端数分をコピー REP STOSB ; REPプレフィックスを用いて連続コピー ;POPFD ; DFフラグを元に戻す POP EDI ; EDIを元に戻す MOV EAX, [ESP+04h] ; return s RET } } int tmp[4*1024*1024]; int main() { int i; const int NUM = 3000; time_t before, after; before = time( NULL ); for( i = 0 ; i < NUM ; ++i ) memset1( tmp, 42, sizeof( tmp ) ); after = time( NULL ); printf( "old memset: %fsec\n", difftime( after, before ) ); before = after; for( i = 0 ; i < NUM ; ++i ) memset2( tmp, 42, sizeof( tmp ) ); after = time( NULL ); printf( "new memset: %fsec\n", difftime( after, before ) ); before = after; for( i = 0 ; i < NUM ; ++i ) memset( tmp, 42, sizeof( tmp ) ); after = time( NULL ); printf( "lib memset: %fsec\n", difftime( after, before ) ); return 0; }
結果は私の環境でどれも30秒ほど。
どれもそれほど大差ない感じでしたがtime関数で精度は不明なので本来ならネイティブなAPIを呼んでなおかつ複数回試すのがいいのでしょうけど面倒でやる気が起きなかったので今回は試してません。
4倍とはいかないでも2倍ぐらいにはなってくれるかなぁ〜っと思ったのですが、ほとんど変わらなかったことを考えると内部でちゃんと対処してくれているのか他のところがボトルネックになってるのかなぁ〜って思いました。
ただ、さっきも書いたようにちゃんと検証したわけではないのでもしかしたらそれなりの差は出てるのかもしれませんが・・・