2013/10/03
bitset
Gaucheプログラミングの小ネタ。
小さい非負整数の集合同士のintersectionを高速に計算しないとならない用事が出来た。
例えば (0 1 4 7 9)
と (2 3 7 8 9 16)
から (7 9)
を求める、
といった話。データは100万個のオーダー。
整数値の範囲は今のところ最大でも50くらい、
今後増えることはありえるがそう極端に大きくはならない。
こういう話なら、整数の集合をビット集合に変換してビット毎のandを取るのが素直だろう。
整数値のリストからビット集合への変換はfold
ですぐできる。
(define (integer-list->bitset ints) (fold (cut copy-bit <> <> #t) 0 ints)))
gosh> (integer-list->bitset '(0 3 4 9 15)) 33305
ビット集合からリストに戻す方はどうだろう。srfi-60のinteger->list
を
使うと整数の各ビットをbooleanにしたリストが得られるが、
右から0,1,2,...と数えないとならないので一旦reverseするのがちと煩わしい。
(define (bitset->integer-list bits) (filter-map (^[b n] (and b n)) (reverse (integer->list bits)) (lrange 0)))
gosh> (bitset->integer-list 33305) (0 3 4 9 15)
gauche.genreator
には整数を真偽値のジェネレータに変換する手続きがある。
bits->generator
は右からビットを取り出すのでそのまま使えるけど、
ジェネレータをリストに変換してやらないとならない。
(define (bitset->integer-list2 bits) (filter-map (^[b n] (and b n)) (generator->list (bits->generator bits)) (lrange 0)))
あるいは、ジェネレータのまま変換していって最後にリストに戻すか。
(define (bitset->integer-list3 bits) (generator->list (gfilter-map (^[b n] (and b n)) (bits->generator bits) (grange 0))))
結果を整数の集合と考えるなら数値の順序はどうでも良いはずだ。
それならば、先に最大ビット番号を求めておいて、そこから逆順に畳み込む手もある。
そしたらinteger->list
の結果をそのまま使える。
ただ、逆順の整数リストを作るのがちと煩わしい。
(define (bitset->integer-list4 bits) (let1 len (integer-length bits) (filter-map (^[b n] (and b n)) (integer->list bits) (iota len (- len 1) -1)))) gosh> (bitset->integer-list4 33305) (15 9 4 3 0)
さてどれが速いだろうか。適当にランダムデータを生成してベンチマーク。
(use data.random) (use gauche.time) (let1 testdata (generator->list (integers$ (expt 2 50)) 100000) ($ time-these/report 1 `((srfi-60 . ,(^[] (for-each bitset->integer-list testdata))) (gen1 . ,(^[] (for-each bitset->integer-list2 testdata))) (gen2 . ,(^[] (for-each bitset->integer-list3 testdata))) (rev . ,(^[] (for-each bitset->integer-list4 testdata))))))
結果:
Benchmark: ran srfi-60, gen1, gen2, rev, each for 1 times. srfi-60: 4.384 real, 4.380 cpu (4.370 user + 0.010 sys)@0.23/s n=1 gen1: 3.865 real, 3.860 cpu (3.860 user + 0.000 sys)@0.26/s n=1 gen2: 5.648 real, 5.650 cpu (5.650 user + 0.000 sys)@0.18/s n=1 rev: 3.510 real, 3.510 cpu (3.510 user + 0.000 sys)@0.28/s n=1 Rate srfi-60 gen1 gen2 rev srfi-60 0/s -- 0.881 1.290 0.801 gen1 0/s 1.135 -- 1.464 0.909 gen2 0/s 0.775 0.683 -- 0.621 rev 0/s 1.248 1.100 1.610 --
data.random
は新しいモジュールで、ランダムデータの生成ツールが色々
揃っている。integer$
は0から与えられた数までの間の整数値を
一様分布で生成するジェネレータを作る関数。ここでは条件を揃えるために
一旦リストに直して各関数に与えた。
最後のやつは、lrange
で降順のシーケンスが作れないので
iota
を使うためにちょっとごちゃっとしたんだけど、lrange
で
降順のシーケンスを作れないようにしたのは何でだったかな? 何か理由があって
意識的にそうした覚えがあるんだけど…
Tags: Programming, Gauche
Post a comment