Island Life

< ピアノレッスン108回目 | mmap >

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

Name: