素晴らしい。
2009-07-04 - 当面C#と.NETな記録問題の説明はここまでにして、コードの紹介です。Hacker's delight のコードより4〜5倍速く、そして、イミフ加減が半端じゃない!これ一つで 64bit 値以下のすべての値に対応できます。
でも、実際にどれくらい威力があるか試してみたかったのでCに移植してみた。意外な結果が出ております。
0x03F566ED27179461ULL
まずは黒魔術。より黒魔術っぽくしてみました。
typedef unsigned long long U64; #define HASH 0x03F566ED27179461ULL static int ntzhash[64]; void init_ntzhash(){ U64 h = HASH; int i; for (i = 0; i < 64; i++, h <<= 1) ntzhash[ h >> 58 ] = i; } int ntz_hash(U64 n){ return n ? ntzhash[ (int)( ( ( n & -n ) * HASH ) >> 58) ] : 64; }
右シフト
最もナイーブな実装ですね。O(n)
int ntz_shift(U64 n){ int i; for (i = 0; i < 64; i++) if ( (n >> i) & 1 ) return i; return 64; }
二分探索
Hacker's delight P. 85に出てきます。O(log n)、64bitの場合最高8回の条件分岐ということになります。三項演算子を使った方が見やすいのでそうしました。
int ntz_bsrch( U64 n ) { n &= -n; return n == 0 ? 64 : n == 0x80000000ULL ? 31 : (n < 0x80000000ULL ? n == 0x8000ULL ? 15 /* 60行ほど中略 */ : (n < 0x4000000000000000ULL ? 61 : 63)))))); }
こんなひちめんどくさいものは手で書いていられないので、以下のPerl Scriptに自動生成してもらいました(要use64bitint)。
sub foo{ my ($head, $tail) = @_; my $mid = int(($head + $tail) / 2); return $mid if $head >= $tail; my $ret = sprintf "n == 0x%xULL ? %d\n", 2**$mid, $mid; $ret .= sprintf ": (n < 0x%xULL ? %s\n: %s)", 2**$mid, foo($head, $mid-1), foo($mid+1, $tail); $ret =~ s/\n+/\n/g; $ret; } print foo(0,63)
frexp
n & -n
で、最低位ビットだけ立てられるということは、二のベキ乗にすでになっているわけです。そこからどうやってビット数にマップするかが、黒魔術のすごいところなのですが、もう少しけれんみのないやり方として、frexp()
を使うというやり方もあります。
#include <math.h> int ntz_frexp(U64 n){ int z; if ( n == 0 ) return 64; frexp( (double)( n & -n ), &z); return z - 1; }
x86のbsf命令
で、x86のbsf命令を使うやり方です。どのx86でも動くよう、ここでは32bit以上なのか否かで条件分岐させています。
int ntz_bsf(U64 n){ if (n == 0) return 64; unsigned int lo = n & 0xffffffff; int z; if (lo){ __asm__("bsf %1, %0;" :"=r"(z) :"r"(lo)); return z; }else{ n >>= 32; __asm__("bsf %1, %0;" :"=r"(z) :"r"(n)); return z + 32; } }
まとめてベンチマーク
で、この5つをまとめてベンチマークしてみました。引数に繰り返し回数を指定するとベンチマーク、何もなければテストになります。Codepadでも動きました。
http://codepad.org/9UXR2cOu#include <sys/time.h> double time_hires(){ struct timeval now; gettimeofday(&now, NULL); return now.tv_sec + now.tv_usec/1e6; } #include <stdio.h> #include <stdlib.h> #include <assert.h> #define D63 ((U64)1 << 63) typedef struct { char *name; int (*func)(U64); } nfpair; static nfpair ntz[] = { {"ntz_shift", ntz_shift }, {"ntz_bsrch", ntz_bsrch }, {"ntz_frexp", ntz_frexp }, {"ntz_hash", ntz_hash }, {"ntz_bsf", ntz_bsf } }; int main (int argc, char **argv){ U64 n; int i, j, count = 0; double timer; if (ntzhash[63] != 6) init_ntzhash(); if (argc > 1){ /* benchmark */ count = (int)strtod(argv[1], NULL); for (j = 0; j < 5; j++){ timer = time_hires(); for (i = 0; i < count; i++){ assert(ntz[j].func(D63) == 63); } printf("%9s: %gs\n", ntz[j].name, time_hires() - timer); } }else{ /* test */ printf("%d\n", ntz_bsrch(D63)); for (j = 0; j < 5; j++){ for (i = 0, n = 1 ; i <= 64; i++, n += n){ assert(ntz[j].func(n) == i); printf("ok %d # %s(0x%016llx) = %d\n", ++count, ntz[j].name, n, ntz[j].func(n)); } } printf("1..%d\n", count); } return 0; }
で、以下が結果です。
Core 2 Duo 2.4GHz, Mac OS X v10.5.7% gcc -W -Wall -O6 ntz.c && ./a.out 1e8 ntz_shift: 16.1367s ntz_bsrch: 1.8118s ntz_frexp: 2.79462s ntz_hash: 1.18244s ntz_bsf: 0.475164sDual Xeon 2.66GHz, FreeBSD 7.2
% gcc -W -Wall -O6 ntz.c && ./a.out 1e8 ntz_shift: 25.2944s ntz_bsrch: 1.65888s ntz_frexp: 7.1559s ntz_hash: 1.50684s ntz_bsf: 0.905658s
ベンチマークで使っている数値は、O(c)でないアルゴリズムで最悪になるよう、1<<63にしてあります。見ての通り、二分探索と黒魔術の差が以外と出てません。64bitの乗算のコストかなあ....
おまけ
Perlの一行野郎だとこんなでしょうか。0の場合も対応しています。一行野郎の割には素直すぎる....
sub ntz{ length(sprintf("%064b", shift) =~ /(0+)$/ && $1) }
Dan the Bit Counter
> n & -nで、最低位ビットだけ立てられるということは、二のベキ乗にすでになっているわけです。
この段階で 0x08 とかになるのであれば 1 を引けば 0x07 です。0x07にいくつ1が立っているのか、もとめる方法が Hacker's delight には書いてあったかと思いますが…。
frexp やそれに類する方式は、「整数→浮動小数点」変換速度(ライブラリなのかCPU命令なのかはともかくとして)に強く依存するので、それだけで不利だと思います。