速度テスト

せっかくPCを新調したのでどれくらい速いのか見てみたいと思う。とりあえずCPUで実行する処理を適当にでっちあげてなんとなく最適化してどれくらいの速度になるのか見てみたい。


お題が満たすべき条件は

  • 最適化しやすい(マルチメディア系処理が良い?)
  • そこそこ重い(CPUで素のまま動かすにはちょっとだけ重い)

あたりかな。

とりあえず適当に

  • 16x32画素の32ビット画像の重ね合わせ処理
  • 重ね合わせはマスク画像をもとにする
  • 1フレームあたり85枚の画像を60フレーム/秒で処理する

って感じで、1秒間に約10MBのデータを処理するって感じで。


この程度ならGPUじゃなくて、CPUで処理するのが順当だろうしちょっとだけ最適化で遊んでみたくなる感じないかなぁ。まぁ実際にはこの程度なら最適化不要かもしれないけどそこはとりあえずのお題ってことで。


アルゴリズム

  • (画像1 AND マスク) OR (画像2 AND 反転マスク)

で。これならなんかSSEとかで最適化しやすそう。


ざざっと書いてみたC言語の処理部はこれ。C言語とはいえ、そこそこ速くなるように64ビットずつ処理する。

#include <stddef.h>
void *masked_memcpyC(void *dest, const void *src, const void *mask, size_t n)
{
	unsigned long		*d8;
	const unsigned long	*s8, *m8;
	size_t			i8;
	unsigned char		*d1;
	const unsigned char	*s1, *m1;
	size_t			i1;


	if(dest == NULL){
		return dest;
	}
	if(src == NULL){
		return dest;
	}
	if(mask == NULL){
		return dest;
	}

	/* 8byte ずつ処理 */
	d8 = (unsigned long *)dest;
	s8 = (const unsigned long *)src;
	m8 = (const unsigned long *)mask;
	for(i8 = 0 ; i8 < (n / sizeof(unsigned long)) ; i8++,d8++,s8++,m8++){
		*d8 = (*d8 & *m8) | (*s8 & (~*m8));		/* 画像1 AND マスク + 画像2 AND 反転マスク */
	}

	/* 1byte ずつ処理 */
	d1 = (unsigned char *)d8;
	s1 = (const unsigned char *)s8;
	m1 = (const unsigned char *)m8;
	for(i1 = 0 ; i1 < (n % sizeof(unsigned long)) ; i1++,d1++,s1++,m1++){
		*d1 = (*d1 & *m1) | (*s1 & (~*m1));
	}

	return dest;
}

それからmain部。
速度計測はRDTSC命令で所要クロック数を計測する感じで。
せっかくなのでC言語版、アセンブラ版、関係ないけど比較用にglibcのmemcpyの3つで計測。

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>

__attribute__((always_inline)) __attribute__((fastcall)) static unsigned long long get_clock_register(void)
{
	unsigned int	eax;
	unsigned int	edx;

	__asm__ volatile ("rdtsc" : "=a"(eax), "=d"(edx));

	return ((unsigned long long)edx << 32) | eax;
}


extern void *masked_memcpyC(void *dest, const void *src, const void *mask, size_t n);
extern void *masked_memcpy(void *dest, const void *src, const void *mask, size_t n);


int main(int argc, char *argv[])
{
	unsigned char	*src;
	unsigned char	*dest;
	unsigned char	*mask;
	unsigned long	size;
	unsigned long	repetition;

	unsigned long long	clock;
	size_t		n;
	unsigned long	c;


	if(argc != 3){
		printf("Usage:\n\t> %s size repetition\n", argv[0]);
		return -1;
	}


	size = strtoul(argv[1], NULL, 0);
	if(size == 0){
		size = 16 * 32 * 4;
	}
	repetition = strtoul(argv[2], NULL, 0);
	if(repetition == 0){
		repetition = 85 * 60;
	}
	n = size * repetition;

	src = (unsigned char *)malloc(n);
	dest = (unsigned char *)malloc(n);
	mask = (unsigned char *)malloc(size);

	{
		int	i;
		for(i=0;i<n;i++){
			src[i] = rand() >> 8;
		}
		for(i=0;i<n;i++){
			dest[i] = rand() >> 8;
		}
		for(i=0;i<size;i++){
			mask[i] = rand() >> 8;
		}
	}


	clock = get_clock_register();
	for(c=0;c<repetition;c++){
		masked_memcpyC(dest+size*c, src+size*c, mask, size);
	}
	printf("C   -> %d\n", (int)(get_clock_register() - clock));
	clock = get_clock_register();
	for(c=0;c<repetition;c++){
		masked_memcpy(dest+size*c, src+size*c, mask, size);
	}
	printf("ASM -> %d\n", (int)(get_clock_register() - clock));
	clock = get_clock_register();
	for(c=0;c<repetition;c++){
		memcpy(dest+size*c, src+size*c, size);
	}
	printf("cpy -> %d\n", (int)(get_clock_register() - clock));


	free(src);
	free(dest);
	free(mask);

	return 0;
}

最後にSSEで最適化もどき。128ビットずつ処理。
「Intel64 and IA-32 Architectures Software Developer's Manual Volume1: Basic Architecture」のChapter10 「Programming with Streaming SIMD Extensions(SSE)」からChapter12「Programming with SSE3, SSSE3, AND SSE4」あたりを眼を皿のようにして使えそうな命令を探しつつ、同「Optimization Reference Manual」のAppendix C C.3.1に書いてあるTable C-x達と見比べつつChapter3「General Optimization Guidelines」あたりを参考にしながら書いてみた。

#void *masked_memcpy(dest, src, mask, n)
#eax   masked_memcpy(rdi,  rsi, rdx,  rcx)
#{
	.text

.globl masked_memcpy
	.type   masked_memcpy, @function

masked_memcpy:
	movq	%rdi, %r8		# backup
	movq	%rdx, %r9		# backup
	movq	%rbx, %r10	# backup

	cmpq	$0,   %r8		# ぬるぽ
	je	Lend		# ガッ!
	cmpq	$0,   %r9		# ぬるぽ
	je	Lend		# ガッ!
	cmpq	$0,   %rsi	# ぬるぽ
	je	Lend		# ガッ!

	/*
	*	for(i=0;i<(n/16);i++){
	*		16バイトずつ処理;
	*	}
	*/
	testq	%rcx, %rcx		# rcx : n
	je	Lend
	movq	%rcx,  %rax
	shrq	$4,    %rax		# rax : i
	je	Lremain

	.p2align 4,,15			# 2^4 align max 15 bytes pad
Loop16:
	movupd	(%rdi),%xmm0		# *dest
	movupd	(%rdx),%xmm1		# mask
	movupd	(%rsi),%xmm2		# src
	andpd	%xmm1, %xmm0		# dest = dest & mask
	andnpd	%xmm2, %xmm1		# mask = ~mask & src
	orpd	%xmm1, %xmm0		# dest = dest | mask
	movupd	%xmm0, (%rdi)		# *dest

	addq	$16,   %rdi		# dest++
	addq	$16,   %rdx		# mask++
	addq	$16,   %rsi		# src++
	subq	$1,    %rax		# i++
#	cmpq	$0,    %rax		# i < (n/16)
	jne	Loop16


Lremain:
	/*
	*	for(i=0;i<(n%16);i++){
	*		1バイトずつ処理;
	*	}
	*/
	movq	%rcx,  %r11		# r11 : i
	andq	$15,   %r11
	je		Lend

	.p2align 3,,7			# 2^3 align max 7 bytes pad
Loop01:
	movzbl	(%rdx),%eax		# eax = *m
	movzbl	(%rdi),%ebx		# ebx = *d
	subq	$1,    %r11		# i++
	addq	$1,    %rdx		# m++
	andl	%eax,  %ebx		# *d & *m  -> xxx
	notl	%eax			# ~*m
	andb	(%rsi),%al		# *s & ~*m -> yyy
	addq	$1,    %rsi		# s++
	orl	%eax,  %ebx		# ebx = xxx | yyy
	movb	%bl,   (%rdi)		# *d = ebx
	addq	$1,    %rdi		# d++
	cmpq	$0,    %r11		# i < (n%16)
	jne     Loop01

Lend:
	movq	%r8,   %rdi		# restore
	movq	%r9,   %rdx		# restore
	movq	%r10,  %rbx		# restore
	movq	%rdi,  %rax		# return dest
	retq
#}

測定結果がこれ。

所要クロック数 2.66GHz換算(ms)
C 6852069 2.58
ASM 6021533 2.26
memcpy 5477978 2.06

てか、アセンブラあんまり速くない...orz 予定では2倍速くらい出てほしかったのに。どこかがボトルネックになってるのか、最適化の余地が残ってるのか。まぁ、ループアンロールすらしてないけど。
う〜ん。思っている以上に最適化って奥が深いかもしれない。Intelのマニュアルにもいろいろ書いてあったしなぁ。後で実験してみようかな。
ちなみになんでmemcpyが遅いんだろ???1バイトずつ転送してるのかな。やってることは単純なのでC言語版の数倍速でても良いハズなんだけど。