続・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倍ぐらいにはなってくれるかなぁ〜っと思ったのですが、ほとんど変わらなかったことを考えると内部でちゃんと対処してくれているのか他のところがボトルネックになってるのかなぁ〜って思いました。
ただ、さっきも書いたようにちゃんと検証したわけではないのでもしかしたらそれなりの差は出てるのかもしれませんが・・・