Benchmark の取り方

なんか頼まれたし書いておくか。結論から言えば、迷って benchmark をとって、騙されていたら世話無いという。
とりあえず、Dan さんのコードをちょいと改造。

--- pow.c.orig  2007-12-14 17:58:05.000000000 +0900
+++ pow.c       2007-12-14 18:11:37.000000000 +0900
@@ -33,7 +33,7 @@
 int main(int argc, char *argv[]){
     U32 i, j;
     U64 rl, rn, rm;
-    double tl, tn, tm;
+    double tl, tn, tm, tc;
     for (i = 0; i < 40; i++) {
         tl = time_hires();
         for (j = 0; j < REPEAT; j++) rl = uipower_o_logn(3, i);
@@ -46,12 +46,16 @@
         tm = time_hires();
         for (j = 0; j < REPEAT; j++) rm = uipower_libm(3, i);
         tm = time_hires() - tm;
+
+        tc = time_hires();
+        for (j = 0; j < REPEAT; j++);
+        tc = time_hires() - tc;
 #if DEBUG
-        printf("uipower(3,%u)=%llu/%llu/%llu\t%.0f\t%.0f\t%0.f\n",
-               i, rl, rn, rm, tl*1e6, tn*1e6, tm*1e6);
+        printf("uipower(3,%u)=%llu/%llu/%llu\t%.0f\t%.0f\t%0.f\t%0.f\n",
+               i, rl, rn, rm, tl*1e6, tn*1e6, tm*1e6, tc*1e6);
 #else
-        printf("uipower(3,%u)=%llu\t%u\t%.0f\t%.0f\t%0.f\n",
-               i, rl, i, tl*1e6, tn*1e6, tm*1e6);
+        printf("%3u%10.f%10.f%10.f%10.f\n",
+               i, tl*1e6, tn*1e6, tm*1e6, tc*1e6);
 #endif
      }
 }

Mac OS 10.4.10 on Intel Core Duo 1.66 GHz、gcc 4.0.1 でコンパイル、実行してみる。

  0     25299     21633     13656     13641
  1     36927     41415     13648     13711
  2     46134     53827     13713     13655
  3     52352     67263     13675     13668
  4     57956     84624     13680     13683
  5     67033     93066     13677     13712
  6     61490    106542     13683     13956
  7     69833    122183     16418     16906
  8     75655    133581     13693     13688
  9     73111    146195     13692     14121
 10     69205    160431     13658     13641
 11     76185    173823     13680     13644
 12     67288    184722     13714     13644
 13     81014    197456     13655     13756
 14     76162    211199     13723     13682
 15    100299    223086     13768     13679
 16     76664    234753     13644     13652
 17     84088    249854     13666     13644
 18     78413    265410     13647     13720
 19     87427    276786     13687     13659
 20     79266    294455     13663     13680
 21    108081    303166     13659     13803
 22     85217    317951     13753     13721
 23     96077    329471     13693     13733
 24     81573    345572     13645     16093
 25    104336    357664     13702     13732
 26     85176    371659     13755     13679
 27     97371    385948     13831     13881
 28     85312    392645     13724     13682
 29     95794    407046     13675     14021
 30     90668    421307     13634     13647
 31     97893    432798     13640     13649
 32     90453    449957     13654     15420
 33     93790    461379     13650     13663
 34     90483    482835     13739     13796
 35     98941    490067     13696     13642
 36     96891    508946     13676     13695
 37    100228    521318     14460     13666
 38     93947    528717     13689     13639
 39    109367    546555     13676     13711

賢明な人ならこの時点で結論が出てますね。
分からないなら -pg を付けてコンパイルしてプロファイルをとった結果でも。

% gcc -O2 -g -pg pow.c -o pow
% ./pow
(snip)
% gprof pow
(snip)
granularity: each sample hit covers 4 byte(s) for 0.12% of 8.45 seconds

  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 61.3       5.18     5.18 40000000     0.00     0.00  _uipower_o_n [3]
 15.7       6.50     1.32 40000000     0.00     0.00  _uipower_o_logn [4]
 12.3       7.54     1.04        1  1040.00  7560.00  _main [1]
  8.4       8.26     0.71                             _moncount (2601)
  1.7       8.39     0.14                             mcount (27)
  0.4       8.43     0.03                             _monreset (2605)
  0.2       8.44     0.01      320     0.05     0.05  _time_hires [5]
  0.1       8.45     0.01                             ___sinit [6]
(snip)

念のためにアセンブリを見ても良い。

% gcc -O2 -S pow.c
% grep -F 'call' pow.s
        call    ___i686.get_pc_thunk.bx
        call    L_gettimeofday$stub
        call    ___i686.get_pc_thunk.bx
        call    L_pow$stub
        call    ___i686.get_pc_thunk.bx
        call    _time_hires
        call    _uipower_o_logn
        call    _time_hires
        call    _time_hires
        call    _uipower_o_n
        call    _time_hires
        call    _time_hires
        call    _time_hires
        call    _time_hires
        call    _time_hires
        call    L_printf$stub

もちろん、インライン展開されたりしている訳ではない。
結論。最適化すると libm の pow が速くなるのではなく、単に最適化によって no effect(無意味) なコードが削除されただけ。というか、既にコンパイルされている共有ライブラリのコードが最適化で速くなるわけが無い*1
自分もやったことがあるけど、最適化がされる場合に、benchmark をとるときは対象のコードが「実際に実行されているか」を確認しないと駄目。ローカル変数への代入くらいだとコンパイラは割と簡単に無意味であることを検知してしまうので、そのあたりを意識しないと無意味な benchmark になってしまう。

追記

いろいろ説明するのが面倒なのでコードとアセンブリを全部掲載しておく。なお、Darwin では -fno-math-errno がデフォルトなので Mac OS X 以外の人は -fno-math-errno をつけて実験すると良い。下のコードは gcc-4.2 on coLinuxコンパイルしてある。

#include <stdio.h>
#include <math.h>
#include <sys/time.h>

#if !defined(REPEAT)
#define REPEAT ((int) 1e6)
#endif

typedef unsigned int U32;
typedef unsigned long long U64;

double time_hires()
{
    struct timeval now;
    gettimeofday(&now, NULL);
    return now.tv_sec + now.tv_usec / 1e6;
}

U64 uipower_o_logn(U64 b, U64 n)
{
    U64 result = 1;
    for (; n != 0 ; b *= b, n >>= 1) if (n & 1) result *= b;
    return result;
}

U64 uipower_o_n(U64 b, U64 n)
{
    U64 result = 1;
    for (; n != 0 ; n--) result *= b;
    return result;
}

U64 uipower_libm(U64 b, U64 n)
{
    U64 result = pow(b, n);
    return result;
}

int main()
{
    U32 i, j;
    U64 rl, rn, rm;
    double tl, tn, tm;
    for (i = 0; i < 40; i++) {
        tl = time_hires(); 
        for (j = 0; j < REPEAT; j++) rl = uipower_o_logn(3, i);
        tl = time_hires() - tl;

        tn = time_hires(); 
        for (j = 0; j < REPEAT; j++) rn = uipower_o_n(3, i);
        tn = time_hires() - tn;

        tm = time_hires(); 
        for (j = 0; j < REPEAT; j++) rm = uipower_libm(3, i);
        tm = time_hires() - tm;
#if DEBUG
        printf("uipower(3,%u)=%llu/%llu/%llu\t%.0f\t%.0f\t%0.f\n",
            i, rl, rn, rm, tl*1e6, tn*1e6, tm*1e6);
#else
        printf("uipower(3,%u)=%llu\t%u\t%.0f\t%.0f\t%0.f\n",
            i, rl, i, tl*1e6, tn*1e6, tm*1e6);
#endif
    }
}
	.file	"pow.c"
	.text
	.p2align 4,,15
.globl uipower_o_logn
	.type	uipower_o_logn, @function
uipower_o_logn:
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%edi
	pushl	%esi
	pushl	%ebx
	subl	$20, %esp
	movl	20(%ebp), %edi
	movl	16(%ebp), %esi
	movl	8(%ebp), %ecx
	movl	12(%ebp), %ebx
	movl	%edi, %eax
	orl	%esi, %eax
	movl	$1, -24(%ebp)
	movl	$0, -20(%ebp)
	jne	.L4
	jmp	.L7
	.p2align 4,,7
.L11:
	movl	%ebx, %eax
	imull	%ecx, %eax
	addl	%eax, %eax
	movl	%eax, -28(%ebp)
	movl	%ecx, %eax
	mull	%ecx
	movl	%edx, %ebx
	movl	%eax, %ecx
	addl	-28(%ebp), %ebx
.L4:
	movl	%esi, %eax
	andl	$1, %eax
	testl	%eax, %eax
	je	.L5
	movl	-20(%ebp), %edx
	movl	-24(%ebp), %eax
	imull	%ecx, %edx
	movl	%edx, -28(%ebp)
	movl	-24(%ebp), %edx
	imull	%ebx, %edx
	addl	%edx, -28(%ebp)
	mull	%ecx
	movl	%eax, -24(%ebp)
	movl	-28(%ebp), %eax
	addl	%eax, %edx
	movl	%edx, -20(%ebp)
.L5:
	shrdl	$1, %edi, %esi
	shrl	%edi
	movl	%edi, %edx
	orl	%esi, %edx
	jne	.L11
.L7:
	movl	-24(%ebp), %eax
	movl	-20(%ebp), %edx
	addl	$20, %esp
	popl	%ebx
	popl	%esi
	popl	%edi
	popl	%ebp
	ret
	.size	uipower_o_logn, .-uipower_o_logn
	.p2align 4,,15
.globl uipower_o_n
	.type	uipower_o_n, @function
uipower_o_n:
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%edi
	pushl	%esi
	pushl	%ebx
	subl	$20, %esp
	movl	20(%ebp), %ebx
	movl	16(%ebp), %ecx
	movl	8(%ebp), %esi
	movl	12(%ebp), %edi
	movl	%ebx, %eax
	orl	%ecx, %eax
	movl	$1, -24(%ebp)
	movl	$0, -20(%ebp)
	je	.L15
	.p2align 4,,7
.L16:
	movl	-20(%ebp), %edx
	movl	-24(%ebp), %eax
	imull	%esi, %edx
	movl	%edx, -28(%ebp)
	movl	-24(%ebp), %edx
	imull	%edi, %edx
	addl	%edx, -28(%ebp)
	mull	%esi
	movl	%eax, -24(%ebp)
	movl	-28(%ebp), %eax
	addl	%eax, %edx
	addl	$-1, %ecx
	adcl	$-1, %ebx
	movl	%edx, -20(%ebp)
	movl	%ebx, %edx
	orl	%ecx, %edx
	jne	.L16
.L15:
	movl	-24(%ebp), %eax
	movl	-20(%ebp), %edx
	addl	$20, %esp
	popl	%ebx
	popl	%esi
	popl	%edi
	popl	%ebp
	ret
	.size	uipower_o_n, .-uipower_o_n
	.p2align 4,,15
.globl uipower_libm
	.type	uipower_libm, @function
uipower_libm:
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%ebx
	subl	$52, %esp
	movl	20(%ebp), %edx
	movl	16(%ebp), %eax
	movl	8(%ebp), %ecx
	movl	12(%ebp), %ebx
	pushl	%edx
	pushl	%eax
	fildll	(%esp)
	addl	$8, %esp
	testl	%edx, %edx
	js	.L25
	fstpl	8(%esp)
	pushl	%ebx
	pushl	%ecx
	fildll	(%esp)
	addl	$8, %esp
	testl	%ebx, %ebx
	js	.L26
.L21:
	fstpl	(%esp)
	call	pow
	flds	.LC1
	fxch	%st(1)
	fucom	%st(1)
	fnstsw	%ax
	fstp	%st(1)
	sahf
	jae	.L22
	fnstcw	-18(%ebp)
	movzwl	-18(%ebp), %eax
	movb	$12, %ah
	movw	%ax, -20(%ebp)
	fldcw	-20(%ebp)
	fistpll	-32(%ebp)
	fldcw	-18(%ebp)
	movl	-28(%ebp), %ebx
	movl	-32(%ebp), %ecx
	addl	$52, %esp
	movl	%ebx, %edx
	popl	%ebx
	movl	%ecx, %eax
	popl	%ebp
	ret
	.p2align 4,,7
.L22:
	fnstcw	-18(%ebp)
	fsubs	.LC1
	movzwl	-18(%ebp), %eax
	movb	$12, %ah
	movw	%ax, -20(%ebp)
	fldcw	-20(%ebp)
	fistpll	-32(%ebp)
	fldcw	-18(%ebp)
	movl	-28(%ebp), %ebx
	movl	-32(%ebp), %ecx
	addl	$52, %esp
	leal	-2147483648(%ebx), %edx
	movl	%edx, %ebx
	movl	%ecx, %eax
	movl	%ebx, %edx
	popl	%ebx
	popl	%ebp
	ret
	.p2align 4,,7
.L25:
	fadds	.LC0
	fstpl	8(%esp)
	pushl	%ebx
	pushl	%ecx
	fildll	(%esp)
	addl	$8, %esp
	testl	%ebx, %ebx
	jns	.L21
	.p2align 4,,7
.L26:
	fadds	.LC0
	jmp	.L21
	.size	uipower_libm, .-uipower_libm
	.p2align 4,,15
.globl time_hires
	.type	time_hires, @function
time_hires:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	leal	-8(%ebp), %eax
	movl	$0, 4(%esp)
	movl	%eax, (%esp)
	call	gettimeofday
	fildl	-8(%ebp)
	fildl	-4(%ebp)
	fdivs	.LC3
	leave
	faddp	%st, %st(1)
	ret
	.size	time_hires, .-time_hires
	.section	.rodata.str1.4,"aMS",@progbits,1
	.align 4
.LC4:
	.string	"uipower(3,%u)=%llu\t%u\t%.0f\t%.0f\t%0.f\n"
	.text
	.p2align 4,,15
.globl main
	.type	main, @function
main:
	leal	4(%esp), %ecx
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%edi
	xorl	%edi, %edi
	pushl	%esi
	xorl	%esi, %esi
	pushl	%ebx
	pushl	%ecx
	subl	$104, %esp
.L31:
	movl	%esi, -52(%ebp)
	movl	$1, %ebx
	call	time_hires
	fstpl	-40(%ebp)
	movl	%esi, 8(%esp)
	movl	%edi, 12(%esp)
	movl	$3, (%esp)
	movl	$0, 4(%esp)
	call	uipower_o_logn
	.p2align 4,,7
.L32:
	movl	%esi, 8(%esp)
	addl	$1, %ebx
	movl	%edi, 12(%esp)
	movl	$3, (%esp)
	movl	$0, 4(%esp)
	call	uipower_o_logn
	cmpl	$1000000, %ebx
	movl	%eax, -48(%ebp)
	movl	%edx, -44(%ebp)
	jne	.L32
	call	time_hires
	movl	$1, %ebx
	fstpl	-72(%ebp)
	call	time_hires
	fstpl	-32(%ebp)
	movl	%esi, 8(%esp)
	movl	%edi, 12(%esp)
	movl	$3, (%esp)
	movl	$0, 4(%esp)
	call	uipower_o_n
	.p2align 4,,7
.L34:
	movl	%esi, 8(%esp)
	addl	$1, %ebx
	movl	%edi, 12(%esp)
	movl	$3, (%esp)
	movl	$0, 4(%esp)
	call	uipower_o_n
	cmpl	$1000000, %ebx
	jne	.L34
	call	time_hires
	fstpl	-64(%ebp)
	call	time_hires
	fstpl	-24(%ebp)
	call	time_hires
	fsubl	-24(%ebp)
	flds	.LC3
	fmul	%st, %st(1)
	fxch	%st(1)
	fstpl	36(%esp)
	fldl	-64(%ebp)
	fsubl	-32(%ebp)
	fstl	-64(%ebp)
	fmul	%st(1), %st
	fstpl	28(%esp)
	fldl	-72(%ebp)
	fsubl	-40(%ebp)
	fmulp	%st, %st(1)
	fstpl	20(%esp)
	movl	-52(%ebp), %eax
	movl	%eax, 16(%esp)
	movl	-44(%ebp), %edx
	movl	-48(%ebp), %eax
	movl	%edx, 12(%esp)
	movl	%eax, 8(%esp)
	movl	-52(%ebp), %edx
	movl	$.LC4, (%esp)
	movl	%edx, 4(%esp)
	call	printf
	addl	$1, %esi
	movl	%esi, %eax
	adcl	$0, %edi
	xorl	$40, %eax
	orl	%edi, %eax
	jne	.L31
	addl	$104, %esp
	popl	%ecx
	popl	%ebx
	popl	%esi
	popl	%edi
	popl	%ebp
	leal	-4(%ecx), %esp
	ret
	.size	main, .-main
	.section	.rodata.cst4,"aM",@progbits,4
	.align 4
.LC0:
	.long	1602224128
	.align 4
.LC1:
	.long	1593835520
	.align 4
.LC3:
	.long	1232348160
	.ident	"GCC: (GNU) 4.2.3 20071123 (prerelease) (Debian 4.2.2-4)"
	.section	.note.GNU-stack,"",@progbits

*1:最適化していない共有ライブラリを別途用意しているなら別だけど