天泣記

2008-01-02 (Wed)

#1

2008-01-03 (Thu)

#1
#2

まぁ、なんというか tr_TR locale は glibc の中でもこなれていないということはわかった。

#3

で、locale に依存しない関数を用意するのだが、ctype 関連は oniguruma のを使える。で、encoding.h でそのへんを定義して ruby.h から include した。これが適切だったかというと微妙な感じもするが、それはそれとして、makefile の依存関係を更新しなければならない。

が、変化が多すぎて手でやるのは面倒臭すぎる。

というわけで、自動的にやることにした。

gcc -MM とかを使っても良かったのだが、なんとなく別の方法を使ってみる。まず、CFLAGS に -save-temps を足してビルドして、プリプロセッサ適用済みなファイルを用意する。

で、各ファイルを見て、include されたファイルをリストアップすれば依存関係がわかるので、makefile にあわせてフォーマットする。

#!/usr/bin/ruby

# make all golf
# ./UPDATE-DEP > z
# diff -u common.mk z
# mv z common.mk

src = File.read("common.mk")
src.gsub!(/^([0-9a-z._]+)\.\$\(OBJEXT\):(.*\n(?:[ 	].*\n)*)/) {
  matched = $&
  basename = $1
  deps = $2
  dd = deps.dup
  dd.gsub!(/\{\$\(VPATH\)\}/, '')
  dd.gsub!(/\\\n/, ' ')
  dd.gsub!(/thread_\$\(THREAD_MODEL\)/, 'thread_pthread')
  dd = dd.strip.split(/\s+/)
  if !File.file?("#{basename}.o")
    warn "#{basename}.o not found."
  else
    unless File.file? "#{basename}.i"
      puts "#{basename}.i not found."
      next
    end
    incs = []
    File.foreach("#{basename}.i") {|line|
      next unless /^# \d+ "([^"]*)"/ =~ line
      inc = $1
      next if %r{\A[/<]} =~ inc
      incs << File.basename(inc)
    }
    incs.uniq!
    add = incs - dd
    if !add.empty? || true
      if incs[0] != dd[0]
        raise "first file not matched: #{incs[0].inspect} v.s. #{dd[0].inspect}"
      end
      depline = "#{basename}.$(OBJEXT):"
      incs.each {|inc|
        inc = inc.sub(/\Athread_pthread/, 'thread_$(THREAD_MODEL)')
        depline << " {$(VPATH)}#{inc}"
      }
      lines = []
      while 72 < depline.length && depline.sub!(/\A(.{0,72}|.{72}.*?) /, '')
        lines << $&
      end
      lines << depline
      matched = lines.join("\\\n  ")
      matched << "\n"
    end
  end
  matched
}

puts src

作ってから思い出したが、やっぱりけっきょく、環境によって変化するところはわからない。たとえば、ビルドに使われないファイルの依存関係はわからないとか、環境によって使われるファイルが異なるとか。なので、いくらか細工の必要は生じる。

2008-01-08 (Tue)

#1

箱根

2008-01-09 (Wed)

#1

箱根

2008-01-10 (Thu)

#1

箱根

2008-01-11 (Fri)

#1

プロシンで「正規表現における非包含オペレータの提案」という発表をしてきた。

#2

以前、福澤諭吉の科学の教科書で、紐を二本つり下げると両方地球の中心に向かうから平行にならないということが書いてあると聞いたことがある。

慶應の福澤諭吉コレクションで訓蒙窮理圖解を読めるので、ちょっと確認してみた。

Flash で作ってあってページを指定してリンクできないのがナニであるが <URL:http://project.lib.keio.ac.jp/dg_kul/fukuzawa/flipper/F7-A08-03/book185.html> の 5ページの図がそれっぽい。

2008-01-13 (Sun)

#1

平行にならないのは地球の中心に向かうからじゃなくて、糸の先につけた玉が引き合うから、という話か?

2008-01-18 (Fri)

#1

唐突に、treemap で、長方形以外を扱えるか考えてみる。

長方形は縦に切っても横に切っても長方形である。なので、再帰的な分割がやりやすいわけであるが、長方形以外だとどうだろうか?

角を増やすとうまくない。五角形は直線で切ってもふたつの五角形にはならない。

三角形ならうまくいく。ひとつの頂点をとおる直線で分割すれば、分割結果はふたつの三角形である。

ちょっと描いてみる... うぅむ。あまりおもしろくないな。

2008-01-25 (Fri)

#1

ふと「あなたの知らない printf」というフレーズが頭に浮かんだ。

2008-01-28 (Mon)

#1

ふと、コード変換に完全ハッシュを使うとどうなるか試してみる。

iconv からデータを取り出して gperf 形式でソースを生成

% cat z.rb 
require 'iconv'

puts <<'End'
struct month { char *name; char *eucjp; };
%%
End

0xa1.upto(0xfe) {|h|
  0xa1.upto(0xfe) {|l|
    eucjp = [h, l].pack("CC")
    begin
      utf8 = Iconv.conv("UTF-8", "EUC-JP", eucjp);
      puts "#{utf8.dump}, #{eucjp.dump}"
    rescue Iconv::IllegalSequence
    end
  }
}

puts <<'End'
%%
End
% ruby z.rb > a.gp
% head a.gp
struct month { char *name; char *eucjp; };
%%
"\xE3\x80\x80", "\xA1\xA1"
"\xE3\x80\x81", "\xA1\xA2"
"\xE3\x80\x82", "\xA1\xA3"
"\xEF\xBC\x8C", "\xA1\xA4"
"\xEF\xBC\x8E", "\xA1\xA5"
"\xE3\x83\xBB", "\xA1\xA6"
"\xEF\xBC\x9A", "\xA1\xA7"
"\xEF\xBC\x9B", "\xA1\xA8"

で、gperf で完全ハッシュを生成する。

% time gperf -t a.gp > a.c
gperf -t a.gp > a.c  95.46s user 0.21s system 99% cpu 1:35.74 total

うぅむ、1分半もかかるのか。

コンパイルしてサイズを見てみる。

% gcc -O2 -c a.c
% ls -l a.o 
-rw-r--r--  1 akr akr 1097804 Jan 28 13:28 a.o
% size a.o
   text    data     bss     dec     hex filename
  47976  497272       0  545248   851e0 a.o
% strip a.o 
% ls -l a.o
-rw-r--r--  1 akr akr 545848 Jan 28 13:29 a.o

1M ちょっと。strip すると半分。

まぁ、gperf はこういう用途が対象ではないのだが。

#2

Bob Jenkins の Minimal Perfect Hashing を試してみる。

JIS X 0208 に対応する UTF-8 のバイト列を食わせたところ、生成は一瞬で済む。

生成されたコードも最小完全ハッシュになっているようだ。生成された hash 関数をコンパイルしたオブジェクトコードは 4940バイト。

ハッシュ関数は UTF-8 のバイト列を食わせると 0 から 6878 までの整数を返すが、ハッシュテーブルは別に用意しなければならない。

UTF-8 から EUC-JP の変換を考えると、ハッシュテーブルを引くと EUC-JP の 2バイトが出てくるというのがいいかな。そうすると 6878 * 2 = 13756バイトのテーブルで済む。確認は出てきた EUC-JP のコードを UTF-8 に逆変換してもともとのと比較すればいい。どうせ EUC-JP から UTF-8 のテーブルは必要だし、こちらは最小完全ハッシュを持ち出さなくても密に作れる。

EUC-JP から UTF-8 のテーブルは 94*94*3=26508バイトで、UTF-8 から EUC-JP のハッシュテーブルで 13756バイト、ハッシュ関数が 4940バイトで、計45204バイト。

ASCII部分とかいろいろと省略しているから実際にはもっと必要だろうが、gperf の 1Mバイトっていうのは使いすぎであろう。

gperf は予約語とか、そんなに多くないものを高速にやるのが対象なので、コード変換にはなるべく小さいハッシュ表で済むこっちみたいなのを使うべきということだろうか。

#3

cmph もちょっと使ってみる。

使用法がソースコード生成ではない。

線形時間で minimal perfect hash を生成するですか。まぁ、最悪の話ではないのだろうが...

2008-01-29 (Tue)

#1

bep にたどりつく。

#2

というわけで、EUC-JP <=> UTF-8 の変換表は 2580+13758+17672=34Kbytes くらいで済む気がする。

#3

もしかして誤解していたかな。最小完全ハッシュを求める問題は NP完全問題ではない?

2008-01-31 (Thu)

#1

bep の資料をしばらく眺めて、よくわからなかったところを元ネタの論文を覗いたら実装できるような気がしてきたので実装してみる。

まず、「二つの独立なハッシュ関数 h1, h2 を用意」とあるので、用意しよう。ここでは、二つのハッシュ関数を組にしたクラス HashPair を定義している。

class HashPair
  @saltcounter = "a"

  def self.gensalt
    ret = @saltcounter.dup
    @saltcounter.succ!
    ret
  end
...

複数のハッシュ関数が必要である。だが、自分でハッシュ関数を用意するのは面倒なので、Ruby の String#hash を利用しよう。でも、String#hash はひとつしかない。なので、ちょっと文字列を加えてから String#hash を使うようにしよう。その文字列をちょっとづつ変えていけば、いくらでもハッシュ関数を用意できる。というわけで、そういう用途のために、gensalt は毎回異なる文字列を生成する。

...
  def initialize(range)
    @range = range
    @m = range * 2
    @min1 = 0
    @min2 = range
    @salt1 = HashPair.gensalt
    @salt2 = HashPair.gensalt
  end
...

HashPair.new はひとつの引数 range をとるが、これは資料では m/2 に対応する。gensalt でふたつの文字列を生成して、準備完了である。

...
  def makehash(str)
    h1 = @min1 + (str + @salt1).hash % @range
    h2 = @min2 + (str + @salt2).hash % @range
    [h1, h2]
  end
end

実際に文字列のハッシュをとるときには makehash を使う。これは 0...(m/2) なハッシュ値と (m/2)...m なハッシュ値を組み合わせて返す。

では最小完全ハッシュ関数を作ってみよう。

class MPHF
  def initialize(keys)
    @n = keys.length
    @m2 = ((@n * 2.09).ceil + 1) / 2
    @m = @m2 * 2
    ordered_edges = mapping(keys)
    assigning ordered_edges
    ranking
  end
...

最小完全ハッシュ関数を作るには三つの段階がある。最初の二つの段階で最小でない完全ハッシュ関数を作り、最後のひとつで最小完全ハッシュ関数を仕立てあげる。それぞれの段階が mapping, assigning, ranking メソッドに対応する。

最小完全ハッシュ関数の値域は 0...keys.length であるが、mapping と assigning で作る最小でないやつの値域はそれよりも大きい。具体的には 2.09倍くらい大きい。この 2.09倍という値には意味があるようだが、その根拠はよくわからないのでさておく。ともかくそのように大きくした値が m である。

最初の段階の mapping では、keys からサイクルのない 2部グラフを生成する。

...
  def mapping(keys)
    begin
      hpair = HashPair.new(@m2)
      hs = keys.map {|key| hpair.makehash(key) }
      ordered_edges = check_ascyclic(hs)
    end until ordered_edges
    @hpair = hpair
    ordered_edges
  end
...

ここで生成するグラフは、枝が各 key に対応し、頂点がハッシュ値に対応する。HashPair#makehash に key を渡すと、ハッシュ値がふたつ返ってくるのでこれらが頂点になる。ふたつのハッシュ値は重ならない値域になっているので、生成されるグラフは 2部グラフになる。

そうやって生成したグラフはサイクルを含むかもしれないし含まないかもしれない。含んでいると都合が悪い (かもしれない) ので、含んでいないグラフが生成されるまでハッシュ関数を作りなおして繰り返す。(ここでサイクルを含んでいないグラフが生成される確率が 2.09倍という倍率に関係しているそうな)

サイクルを検査するメソッドが check_ascyclic である。これはグラフにサイクルがない場合、とある順番で枝を並べ直したものを返す。

無向グラフのサイクルを検査するにはいろいろな方法があるが、他の枝がつながっていない頂点をもつ枝をひとつずつ取り除いていき、グラフが最終的に空になるかどうかで検査する。

...
  def check_ascyclic(edges)
    v2es = Array.new(@m) { [] }
    edges.each_with_index {|e, i|
      n1, n2 = e
      v2es[n1] << e
      v2es[n2] << e
    }
...

まず、各頂点についてそれにつながっている枝をリストアップする。

...
    ordered_edges = []
    v2es.each_index {|node|
      next if v2es[node].length != 1
...

各頂点について繰り返すが、頂点につながっている枝がひとつでなければ無視して先を続ける。ひとつならば、以下でそれとそこからつながっているものを可能な限り取り除く。

...
      stack = [node]
...

つながっているのを深さ優先で処理するスタックを作る。

...
      until stack.empty?
        n = stack.pop
        e = v2es[n].first
...

スタックからひとつ頂点を取り出し、それにつながっている枝を取り出す。(スタックの中にはいっている頂点はすべて枝がひとつしかつながっていないので、first とあるけれどそれがすべてである)

...
        ordered_edges << e
        n1, n2 = e
        v2es[n1].delete e
        v2es[n2].delete e
...

枝はその両端の頂点について登録されているので、両方で取り除く。

...
        stack << n1 if v2es[n1].length == 1
        stack << n2 if v2es[n2].length == 1
...

取り除いた結果、一方の頂点につながっている枝は存在しないはずであるが、もう一方にいくつつながっているは不明である。もしひとつしかつながっていなかったらそれを処理するよう、スタックに入れる。(まぁ、スタックじゃなくていいんじゃないかというのはその通りではある。3-ハイパーグラフを使った場合にはスタックが必要なのでこうなっている)

...
      end
    }
    if v2es.any? {|es| !es.empty? }
      return nil # cycle found
    end
...

ループが終わって、枝がひとつでも残っていれば、サイクルが存在するので nil を返す。

...
    ordered_edges
  end
...

サイクルがなければ、ordered_edges を返す。これは枝を取り除いた順番に並べたものである。

このようにしてサイクルがないものが見つかれば、assigning に進む。assigning では、「各枝につき、頂点を1つずつ割り当てる」。

サイクルがない無向グラフなので、これは要するに森である。木がいくつかあるわけである。木に存在する頂点は枝よりもひとつ多いので、割り当てられない頂点がひとつ出てくる。その頂点を根とみなすと、各枝について根につながっていないほうの頂点を割り当てれば良い。

ここで、key に対応しているのが枝で、頂点がハッシュ値に対応しているので、枝に頂点が割り当てられると key にハッシュ値が対応することになる。というわけで、枝を与えられたときにどちらの頂点を選ぶかという情報を記録しておけば、key を与えられたときに二つのハッシュ関数を適用して枝を得て、どちらかを選択することにより他の key と衝突しない値を得ることができる。つまり完全ハッシュ関数ができあがる。

問題はどうやって選択するかであるが、答をいってしまえば、頂点に 0 か 1 の値を割り当て、枝を得たら両端の頂点の値を 2を法として加える。その結果の 0, 1 をふたつのハッシュ関数に対応させる。

根から葉に向かって木をたどると、各頂点でハッシュ関数は交互に使用されるので、たとえば和が 0, 1, 0, 1, 0, 1, ... という数列になるような割り当てにすることができる。そうするための割り当ては 0, 0, 1, 1, 0, 0, 1, 1, ... である。この数列の隣り合ったものを加えると 0, 1, 0, 1, ... となる。

このような割り当てを行うためには根から枝をたどって、割り当てを行う。そのために、check_ascyclic が返した ordered_edges を使う。ordered_edges は、じつは最後の枝の頂点は根とみなしても良いものになっているのである。

というわけで、ordered_edges の最後から割り当てを行う。

...
  def assigning(ordered_edges)
    @g = Array.new(@m, 2)
...

割り当ては @g に記録するが、その初期値は 2 にしておく。2 を法として 2 は 0 であるが、0 と異なり割り当てが行われていないことを示している。

...
    visited = Array.new(@m, false)
    ordered_edges.reverse_each {|e|
      u = e.find {|n| !visited[n] }
...

それぞれの枝について、まだ訪れていない頂点を探す。ここで訪れていない頂点は必ず残っているように枝が並んでいる。

...
      j = e.index(u)
...

その頂点が枝のどっち側の頂点かで使用するハッシュ関数が決まる。j が使いたいハッシュ関数を示している。

...
      e.each {|v|
        next if !visited[v]
        j -= @g[v]
      }
...

各頂点について g の値を足したものが j になって欲しいわけである。なので、j からここで割り当てを行う以外の値を引いて、値をあわせる。

...
      @g[u] = j % 2
...

そんで、2を法としているので 0, 1 に直して割り当てる。

...
      e.each {|v|
        visited[v] = true
      }
...

枝の両端を訪れたものとしてフラグを立てる。

...
    }
  end
...

というようにして割り当てが行われ、完全ハッシュ関数に必要な情報が揃う。

完全ハッシュ関数 (perfect hash function) は以下である。

...
  def phf(key)
    h1, h2 = @hpair.makehash(key)
    i = @g[h1] + @g[h2]
    case (i % 2)
    when 0 then h1
    when 1 then h2
    end
  end
...

ハッシュ値を二つ求め、g でそれぞれに割り当てられた値を加え、ハッシュ値のどちらかを選ぶ。key の長さを気にしないことにすれば、これは定数時間で済む。

こうやってできた完全ハッシュ関数は最小完全ハッシュ関数の 2.09 倍くらい大きな値域をもつので、これをどうにかして縮めれば最小完全ハッシュ関数ができる。要するに、使用していない値を除いた値にすればいいわけで、phf が返した値と 0 の間に有効な値がいくつあるかを数えて、その値を返せば良い。

しかし、ハッシュは定数時間で動いてほしいので、key の数に比例してしまうような数え方は許されない。なので、事前に 256個毎に 0から累積した有効な値の数を記録してテーブルを作る。それが ranking である。

...
  RANK_BLOCKSIZE = 256
  def ranking
    @ranking = []
    k = 0
    @g.each_with_index {|j, i|
      @ranking << k if i % RANK_BLOCKSIZE == 0
      next if j == 2
      k += 1
    }
  end
...

ここで @g に 2 として残っている、割り当てが行われていない、という情報を使っている。

そうやってできた @ranking テーブルを使って、最小完全ハッシュ関数 (minimal perfect hash function) が作れる。

...
  def mphf(key)
    h = phf(key)
    a = h / RANK_BLOCKSIZE
    result = @ranking[a]
    (a * RANK_BLOCKSIZE).upto(h-1) {|i|
      result += 1 if @g[i] != 2
    }
    result
  end
end
...

まぁ、phf の結果を 256で割って、テーブルから累積したのをとってきて、残りを数えているわけである。このループは 256という定数で抑えられているので、定数時間で動くことには変わりない。

いちおう確認してみよう。

...
keys = (1..10).map { rand.to_s }
...

key を適当に用意する。

...
mphf = MPHF.new(keys)
...

最小完全ハッシュ関数を生成する。

...
check = {}
keys.each {|key|
  hash = mphf.mphf(key)
  p [key, hash]
  if check[hash]
    raise "collision: #{hash} : #{check[hash].inspect} #{key.inspect}"
  end
  check[hash] = key
}

key のそれぞれについて最小完全ハッシュ関数を適用して、返ってくる値に重複がないか検査する。

実行すると、例えば以下のようになる。

% ruby mphf.rb
["0.723613576386467", 1]
["0.576711772900182", 5]
["0.705918310484599", 2]
["0.547451826293092", 4]
["0.593221727615754", 6]
["0.444904732250504", 8]
["0.140848444162556", 3]
["0.578232164791411", 0]
["0.986732992090755", 9]
["0.631578866781294", 7]

適当に生成した key の文字列 10個に対し、0 から 9 までの整数を重複なく割り当てる最小完全ハッシュ関数になっている。


[latest]


田中哲