天泣記

2008-03-01 (Sat)

#1

allpairs.rb を実装したので、test_m17n_comb.rb とかで使うようにした。

「水準の異なる因子」というところはよくわからなかった (2つよりも多い種類がある場合に乗算でテストが増えていくような気がした) ので、最大の水準のままで表を組み合わせて大きくして、mod して済ますことにした。まぁ、テストが多めになるかもしれないが、pair を生成しそこねることはないだろう。

そのうち論文にでもあたるか。

2008-03-03 (Mon)

#1

google がいつのまにか RFC どおりな deflate 圧縮を理解するようになっている?

2008-03-04 (Tue)

#1

GCC Bugzilla Bug 35427 pointer subtraction in very big array

2008-03-05 (Wed)

#1

printf("%#g\n", 1.0-DBL_EPSILON/2); で 1.000000 が表示されることに気がついてバグみっけ、と思ったら、新しい glibc では直っているっぽい。

2008-03-07 (Fri)

#1

最近、sprintf とかのテストを書いていて思ったのだが、ここで書いているテストは明らかに TDD には使えない。

もちろん、時系列として、Ruby にはすでに sprintf があり、後からテストを書いているので、TDD ではない。

しかし、時系列の点だけではなく、書いているテストの内容が TDD には使えないのである。

sprintf のテストは以下の構成になっている。

つまり、sprintf を新しく実装して、片っ端から入力を自動生成して食わせて、2つの sprintf の結果を比較する。

これは TDD には使えない。

仮に、このようなテストを TDD で使うことを考えよう。

TDD というからには実装の前にテストを書くわけだが、この場合テストの中に実装が必要である。

テスト中の実装ではテストを使えないので、TDD で期待される、インクリメンタルな作業の進めかたはできない。その状況で完成品と同等の機能を実現しなければならない。

それでは問題が元の問題に戻っている。sprintf を作るには、まず sprintf を作る必要がある、というのでは話が進んでいない。

ということは、TDD のテストは、テストならなんでもいいというわけではない。インクリメンタルに作業を進められるということが必要だとすれば、テストの記述と対応する実装が両方ともそれなりに簡単な必要がある。

となると、そういう簡単なテストを選ぶにはどうするか、というのが問題になる。

#2

長年、後方参照の有用性に疑いを持ってきたのだが、今日、ついに有用である例に出会った。

Ruby スクリプトを入力として、コメントっぽいものを取り出すことを考える。ここで、複数行連続したコメントで、インデントが等しいものを一括して取り出したい。

そういうものにマッチするパターンは、/^([ \t]*)\#.*\n(\1\#.*\n)*/ として記述できる。

まぁ、space と tab の違いがちょっと問題かな。でもそれは最初に展開しておけばいいか。

2008-03-09 (Sun)

#1

Python の doctest はすばらしい、というか、ドキュメント内のコード例をちゃんとしておくにはそういうツールが当然にあってしかるべきだと思う。

というわけで、Ruby 用のをちょっと作って、試してみる。

#2

doctest に関連する話を探して読んでみると、TDD のテストに使うかどうかという点が、いろいろと議論になっている。

もともとの意図は、TDD ではないようだ。(初期の) doctest のドキュメントには、ユニットテストが第一目的ではないと書いてある。あくまでもドキュメントを正しく保つのが意図のようだ。

ただ、ドキュメントに記述すべき例と TDD で記述すべきテストはある程度重なるだろう。ドキュメントの例と TDD のテストは、どちらもプログラムの仕様を記述するものだから、重なるのはおかしくない。

でも、ドキュメントの例のほうが適切な量は少なそうな気がするな。重箱の隅をいちいちすべて説明する必要はないだろうし。

#3

あるていど重なるとして、説明の順番と、実装の順番が一致するかという問題はあるか。

#4

Dave Astels の Beyond Test Driven Development: Behaviour Driven Development を見てみる。

2008-03-12 (Wed)

#1

MurmurHash で大量の衝突を意図的に起こせるか考えてみる。

コードの冒頭の以下のコードで String をスキャンしてハッシュ値を求めている。(この後に 4バイトで割り切れない部分の処理やらなんやらがあるが、そこは繰り返しじゃないので興味がない)

h += 0xdeadbeef;

while(len >= 4) {
    h += *(unsigned int *)data;
    h *= m;
    h ^= h >> r;

    data += 4;
    len -= 4;
}

ここで、h の初期値は 0 なので、ループ開始時には 0xdeadbeef である。(m と r は定数で、それぞれ 0x7fd652ad, 16 である)

もし、ループが何回か回って、h が 0xdeadbeef に戻ってくるようなデータをひとつ見つけられれば、そのデータを持つ文字列は空文字列と等しいハッシュ値を持つ。そして、その文字列を任意回繰り返したものも等しいハッシュ値を持つ。つまり、ある長さ以下の文字列に限定すれば、その長さに比例する数のハッシュ値の等しい文字列をつくり出せる。

もし、0xdeadbeef に戻ってくるデータをふたつ見つけられれば、それらを組み合わせることができる。組合せにより、ある長さ以下の文字列に限定すれば、その長さの指数関数に比例する数のハッシュ値の等しい文字列をつくり出せる。

とりあえず一回の繰り返しでできるかと調べてみると、(2**32 は問題なく brute force で調べられるので) "\x9b\x2c\x93\x5d" という文字列がそうなるようだ。

% ruby -ve 'p "\x9b\x2c\x93\x5d".hash, "".hash'
ruby 1.9.0 (2008-03-12 revision 15755) [i686-linux]
212676728
212676728

ここで 212676728 が 0xdeadbeef じゃないのはハッシュ関数がループの後で値をいじっているからである。

ひとつしかないとすると、これだけだと長さに比例するくらいしか生成できない。もっとたくさん生成するには一回の繰り返しだけではだめなようである。

そうすると二回の繰り返しであるが、2**64 は brute force では歯が立たない。力ずくではなく、頭を使わないといけないようだ。

繰り返しの中のハッシュ値を更新するコードをもっとちゃんと調べてみよう。更新するんじゃなくて古い値 h1 から新しい値 h2 を計算するようにコードを変えたものを以下に示す。

t1 = h1 + d
t2 = t1 * m
h2 = t2 ^ (t2 >> r)

とりあえずやりたいのは、h1 と h2 から d を求めることである。まず、最初の式で移項して d について解く。

d = t1 - h1

というわけで、残りの式を t1 について解ければいい。

h2 と t2 の関係を考えると、32bit 符号なし整数の、上位 16bit はそのままにして、下位 16bit を上位 16 bit と xor するようになっている。上位 16bit はそのままなので、もう一回同じことをすれば元の値に戻る。とするとこうである。

t2 = h2 ^ (h2 >> r)

あとは、t2 から t1 を求められればいい。ここで m = 0x7fd652ad で、2**32 を法とした計算なので、これは一次合同方程式である。

t2 = t1 * 0x7fd652ad    (mod 2**32)

0x7fd652ad と 2**32 は互いに素なので、t2 をひとつ決めれば、t1 はひとつ決まる。

0x7fd652ad の (2**32 を法とした) 逆数は 0x8c4ce125 なので、両辺に 0x8c4ce125 をかけて t1 について解ける。(0x8c4ce125 は brute force で探したのだが、もっとましな方法があるのではないかという気がする)

t2 * 0x8c4ce125 = t1 * 0x7fd652ad * 0x8c4ce125          (mod 2**32)
t2 * 0x8c4ce125 = t1 * (0x7fd652ad * 0x8c4ce125)        (mod 2**32)
t2 * 0x8c4ce125 = t1 * 1                                (mod 2**32)
t2 * 0x8c4ce125 = t1                                    (mod 2**32)
t1 = t2 * 0x8c4ce125                                    (mod 2**32)

と、いうわけで

d = (h2 ^ (h2 >> r)) * 0x8c4ce125 - h1

である。

h1 == h2 == 0xdeadbeef とすると、

d = 0x7a09aff25d932c9b = 0x5d932c9b

であり、"\x9b\x2c\x93\x5d" を little endian として解釈したものに等しい。

h が一回で 0xdeadbeef -> 0xdeadbeef と変化するのはこのひとつしかないわけであるが、二回なら、たとえば 0xdeadbeef -> 0 -> 0xdeadbeef とか、0xdeadbeef -> 1 -> 0xdeadbeef とかが考えられる。まぁ、2**32個くらい。

具体的に 0xdeadbeef -> 0 -> 0xdeadbeef な文字列を求めてみると、

h1 = 0xdeadbeef, h2 = 0 として d = 0x21524111 となり、h1 = 0, h2 = 0xdeadbeef として d = 0x3c40eb8a となる、

これを little endian に並べて hash を求めるとちゃんと衝突する。

% ruby -e 'p "\x11\x41\x52\x21\x8a\xeb\x40\x3c".hash'
212676728

とすると、長さ 8バイト限定で、2**32 個のハッシュ値の等しい文字列が生成できる。

長さを 12 バイトにすれば 0xdeadbeef -> X -> Y -> 0xdeadbeef として、X, Y それぞれを 2**32 個選べるので、2**64 個生成できる。

あ、繰り返さないんであれば、0xdeadbeef に戻さなくてもいいかな。

#2

そーか。2変数の一次不定方程式がユークリッドの互助法で解けるから、それが使えるんだな。

% cat t.rb 
# returns [x,y] such that ax + by = 1
def f(a, b)
  if a == 1
    [1,0]
  elsif b == 1
    [0,1]
  else
    if a < b
      q, r = b.divmod(a)
      x, y = f(a, r)
      x = x - q * y
      [x, y]
    else
      q, r = a.divmod(b)
      x, y = f(r, b)
      y = y - q * x
      [x, y]
    end
  end
end

x, y = f(0x7fd652ad,2**32)
p((x % 2**32).to_s(16))
% ruby t.rb 
"8c4ce125"
#3

うが。後から「ハッカーのたのしみ」の「定数による整除算」という項にやりかたが書いてあったことに気がついた...

2008-03-13 (Thu)

#1

そろそろ cvs.m17n.org を Etch にしないとなぁ。

2008-03-15 (Sat)

#1

数論で学ぶアルゴリズム(仮) - 第1回勉強会・読書会 『ハッカーのたのしみ - 本物のプログラマはいかにして問題を解くか』

お、行こうかな、と思ったら大阪だった...

#2

思い立って、中断していたところからハッカーのたのしみを読み進める。

中断していたのは p.81 で、x が 7bit 整数のときに modu((x*0x00204081)|0x3db6db00, 1152) で 奇数パリティとできる、というところが納得できなかったからである。正確には、ちゃんと式を書いて計算すればわかるだろうなぁ、とおもいつつ、面倒だったのである。

x は 7bit なので、x6, ..., x0 を各ビットとすると以下のようになる。(0b a b c などとあったら a*2**2 + b*2**1 + c*2**0 と思いねぇ)

#                       31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
 x =                                                                                            0b x6 x5 x4 x3 x2 x1 x0
   0x00204081 =                                    0b  1  0  0  0  0  0  0  1  0  0  0  0  0  0  1  0  0  0  0  0  0  1
 x*0x00204081 =                  0b x6 x5 x4 x3 x2 x1 x0 x6 x5 x4 x3 x2 x1 x0 x6 x5 x4 x3 x2 x1 x0 x6 x5 x4 x3 x2 x1 x0
               0x3db6db00 = 0b 1  1  1  1  0  1  1  0  1  1  0  1  1  0  1  1  0  1  1  0  1  1  0  0  0  0  0  0  0  0
(x*0x00204081)|0x3db6db00 = 0b 1  1  1  1 x4  1  1 x1  1  1 x5  1  1 x2  1  1 x6  1  1 x3  1  1 x0 x6 x5 x4 x3 x2 x1 x0

(x*0x00204081)|0x3db6db00
=
2**29 + 2**28 +
2**27 + 2**26 + x4*2**25 +
2**24 + 2**23 + x1*2**22 +
2**21 + 2**20 + x5*2**19 +
2**18 + 2**17 + x2*2**16 +
2**15 + 2**14 + x6*2**13 +
2**12 + 2**11 + x3*2**10 +
2**9 + 2**8 + x0*2**7 +
0b x6 x5 x4 x3 x2 x1 x0
=
0x380 + 0x400 +
0x200 + 0x100 + x4*0x80 +
0x280 + 0x380 + x1*0x400 +
0x200 + 0x100 + x5*0x80 +
0x280 + 0x380 + x2*0x400 +
0x200 + 0x100 + x6*0x80 +
0x280 + 0x380 + x3*0x400 +
0x200 + 0x100 + x0*0x80 +
0b x6 x5 x4 x3 x2 x1 x0
=
0x2580 + x4*0x80 - x1*0x80 + x5*0x80 - x2*0x80 + x6*0x80 - x3*0x80 + x0*0x80 +
0b x6 x5 x4 x3 x2 x1 x0
(mod 0x480)

ここで、加算と減算は最下位ビットだけ見れば両方とも xor なので、0x80 のビットで x0, ... x6 の xor がとれていて、さらに 0x2580 の 0x80 でビット反転し、奇数パリティになっている。

やはりちゃんと式を書けばわかった。

2008-03-16 (Sun)

#1

A+ でちょっと遊んでみる。

2008-03-18 (Tue)

#1

QuickCheck の話で気になるのはランダムに入力を生成するというところである。

all-pairs とかの知見を使わないのはなにか理由があるのだろうか。

#2

キャッシュの大きさとかにかかわらず、大体うまくキャッシュを使うアルゴリズムを Cache-oblivious algorithm というらしい。

#3

Python 3000 and You を眺めてみる。

んー、Argument annotations ってなんだろう?

2008-03-19 (Wed)

#1

FSIJ は今年も Google SoC の mentor をやるのだが、FSIJ 側からの提示するテーマは今週くらいで決めないといけないそうな。

なにかネタはあるだろうか。

2008-03-20 (Thu)

#1

キャッシュを効率的に利用するのにヒルベルト曲線が使われるという話を知って、生成してみたくなる。

とりあえず行列積に使ってみるには 3次元のヒルベルト曲線が必要である。

が、3次元のは生成のしかたがわからない。

2次元のはハッカーのたのしみに載っているし、 Wikipedia にも載っている。検索すれば他にもたくさん出てくる。

Wikipedia には 3次元のヒルベルト曲線の画像も載っている。

が、生成のしかたがなかなか見つからない。

結局、論文を参照した。

A.R. Butz, "Alternative algorithm for Hilbert's space filling curve", IEEE Trans. On Computers, 20:424-426, April 1971.

たった 3ページの論文で、書いてあるとおりに実装すれば動くのだが、なんでそれが正しく動くのかはいまひとつ理解できない。でも動くのでいいとしよう。

% cat hilbert.rb
def hilbert_pos(r, m, n=2)
  nmask = (1 << n) - 1
  #printf "nmask:        %40b\n", nmask

  #printf "r:            %40b\n", r

  lsbs = 0
  m.times {|i|
    lsbs |= 1 << (i * n)
  }
  #printf "lsbs:         %40b\n", lsbs

  msbs = lsbs << (n-1)
  #printf "msbs:         %40b\n", msbs

  has_bit = r
  all_bit = r
  b = 1
  while b*2 < n
    has_bit |= has_bit >> b
    all_bit &= all_bit >> b
    b *= 2
  end
  has_bit = (has_bit >> (n-b)) | has_bit
  has_bit &= lsbs
  all_bit = (all_bit >> (n-b)) & all_bit
  all_bit &= lsbs

  #printf "has_bit:      %40b\n", has_bit
  #printf "all_bit:      %40b\n", all_bit

  jmask = all_bit | (~has_bit & lsbs)
  rest = (~jmask & lsbs)
  jmask |= (((r ^ (r + (rest & r) - (rest & ~r))) & ~lsbs) >> 1) + rest

  #printf "jmask:        %40b\n", jmask

  j = []
  m.times {|i|
    jj = (jmask >> (i * n)) & nmask
    j << n - jj.to_s(2).length
  }
  j.reverse!

  #printf "j:            %40s\n", j.inspect

  sigma = r ^ ((r >> 1) & ~msbs)

  #printf "sigma:        %40b\n", sigma

  tau1 = sigma ^ lsbs
  #printf "tau1:         %40b\n", tau1
  parity = 0
  tmp = tau1
  nn = n
  w = 1
  while 0 < nn
    if nn.odd?
      parity ^= tmp
      tmp >>= w
    end
    tmp ^= tmp >> w
    nn >>= 1
    w <<= 1
  end
  mask = ((lsbs & parity) * ((1 << n) - 1))
  #printf "mask:         %40b\n", mask
  tau = tau1 ^ (mask & jmask)

  #printf "tau:          %40b\n", tau

  sigma2 = 0
  tau2 = 0
  s = 0
  m.times {|i|
    w = n * (m-1 - i)
    t = (sigma >> w) & nmask
    sigma2 |= (((t >> (s-n)) | (t >> s)) & nmask) << w
    t = (tau >> w) & nmask
    tau2 |= (((t >> (s-n)) | (t >> s)) & nmask) << w
    s += j[i]
    s %= n
  }

  #printf "sigma2:       %40b\n", sigma2
  #printf "tau2:         %40b\n", tau2

  omega = 0
  1.upto(m-1) {|i|
    omega ^= tau2 >> (i*n)
  }

  #printf "omega:        %40b\n", omega

  alpha = sigma2 ^ omega

  #printf "alpha:        %40b\n", alpha

  vec = [0] * n

  topmask = nmask << (m-1) * n
  n.times {|k|
    x = alpha & (msbs >> k)
    m.times {|i|
      if (x & (topmask >> i * n)) != 0
        vec[k] |= 1 << (m-1-i)
      end
    }
  }
  vec
end

def hilbert_each(m, n=2)
  0.upto((1 << (n*m))-1) {|i|
    vec = hilbert_pos(i, m, n)
    yield vec
  }
end

# The example in the paper by Butz:
#  p hilbert(0b10011_00010_00101_11000, 4, 5)

if __FILE__ == $0
  m = (ARGV.shift || '3').to_i
  n = (ARGV.shift || '2').to_i

  h = {}
  p0 = nil
  hilbert_each(m, n) {|p1|
    puts p1.join(' ')
    if p0
      if [p0,p1].transpose.inject(0) {|s, (a,b)| s+(a-b).abs } != 1
        raise "non-adjacent point"
      end
    end
    if h[p1]
      raise "duplicate point"
    end
    h[p1] = true
    p0 = p1
  }
end

% ruby hilbert.rb 2 2
0 0
1 0
1 1
0 1
0 2
0 3
1 3
1 2
2 2
2 3
3 3
3 2
3 1
2 1
2 0
3 0

座標を眺めるだけでは楽しくないので可視化してみよう。

こういう形式ならとりあえず gnuplot で可視化できる。

gnuplot> plot [-1:][-1:] '<ruby hilbert.rb 5 2' with lines 

2次元 5次ヒルベルト曲線

3次元も同様である。

gnuplot> splot '<ruby hilbert.rb 3 3' with lines 

3次元 3次ヒルベルト曲線

しかし、3次元の表示結果は、あー、なんというか、端的にいってしまうと、しょぼい。

Wikipedia に載っているような画像は生成できないだろうか。

そう思って探したところ、OpenDX が使えそうなので試してみることにした。

ただ、どうも線を描画するのはあまり想定していないようで、かなり調べてもやりかたがわからなかったのだが、最終的に検索に頼り、 <URL:http://opendx.npaci.edu/mail/opendx-users/2003.05/msg00060.html> というのを見つけてどうにかできた。

まず以下のような curve.dx を作る。

% cat curve.dx 
#positions component (your points)
object "my_pos" class array type float rank 1 shape 3 items 512 data follows
# list your 3-vector positions here, this example has 512, change the number above to match your count
0 0 0
1 0 0
1 0 1
... ruby hilbert.rb 3 3 の結果 ...
6 0 1
6 0 0
7 0 0

#connections component (1-D line in 3-space)
object "my_conn" class gridconnections counts 512 #match this number to point count

#the field
object "my_field" class field
component "positions" value "my_pos"
component "connections" value "my_conn"

そして、Visual Program Editor で Import, Tube, Image モジュールをつなげる。

つなげた様子

Import で curve.dx を読み込むように指定し、Tube でチューブの太さを指定する。

そうやってなんとかできあがった。

OpenDX による 3次元 3次ヒルベルト曲線

2008-03-21 (Fri)

#1

さて、可視化はともかくとして、キャッシュである。

n*n の正方行列の積 a = b * c を単純に書けば、以下のようになる。a の各要素は 0 に初期化済ということにしよう。

n.times {|i|
  n.times {|j|
    n.times {|k|
      a[i,j] += b[i,k] * c[k,j]
    }
  }
}

最内ループの中では、b, c, a の要素を読んで、a の要素に書き戻す。キャッシュの観点からいえば、最後の a の要素の書き戻しでそれがキャッシュに載っている (キャッシュヒットする) のは明らかなので、以降では無視することにする。

そうすると、a, b, c の各行列の各要素のアクセス回数はそれぞれ n回である。1箇所に n回もアクセスするので、キャッシュが有効に働いていただけるとありがたい。

とりあえず各箇所におけるアクセス間隔だけを考えることにして、キャッシュライン等は気にしないことにしよう。

例えばキャッシュのサイズを、行列全体が入る程には大きくない (つまり n*n より小さい) という状況を考える。そうすると、とりあえず c の要素のアクセスは必ずキャッシュミスすることがわかる。c[k,j] の添字 k, j は内側の 2重ループなので、毎回変化し、同じ添字が出てくるのは n*n 回間隔である。そうすると、行列全体はキャッシュに入らないと想定したので、前回アクセスしたときのキャッシュは既に消えており、キャッシュミスとなる。

キャッシュのシミュレーションをしてみよう。無限に大きなキャッシュを考えて、Cell のリストとして実現する。ある場所 (Cell) にアクセスしたとき、キャッシュ内で Cell を並び替え、アクセスした Cell をリストの先頭 (一番最近アクセスされたことを意味する場所) に動かす。そのときに、動かす前にどのくらい古いところに位置していたかを記録する。あるキャッシュサイズを決めると、そのサイズよりも古いところへ移動してしまった Cell は LRU でキャッシュから破棄されたものとみなせる。つまり、動かす前の場所がキャッシュサイズよりも古いところだったときがキャッシュミスと判断できる。

ここで、あるキャッシュサイズを決めたときに、キャッシュヒットとキャッシュミスの割合がどうなるか知りたい。トータルでアクセスが 3*n**3 回なのはわかっているので、キャッシュサイズに対するキャッシュヒットの数がわかればいい。そのためには、あるキャッシュサイズに対して、そのキャッシュサイズ以下の古さのアクセスの数を数えればいい。

class Cell
  @list = Object.new
  class << @list
    attr_accessor :next
  end
  @list.next = nil

  def Cell.register(cell)
    cell.next = @list.next
    cell.prev = @list
    cell.prev.next = cell
    cell.next.prev = cell if cell.next
  end

  def Cell.access(cell)
    cell.prev.next = cell.next
    cell.next.prev = cell.prev if cell.next

    cell.next = @list.next
    cell.prev = @list
    cell.prev.next = cell
    cell.next.prev = cell if cell.next
  end

  def Cell.age(cell)
    n = 0
    until cell == @list
      cell = cell.prev
      n += 1
    end
    n
  end

  def Cell.stat_accumulation
    hist = []
    num_cells = 0
    num_access = 0
    cell = @list.next
    while cell
      num_cells += 1
      num_access += 1 + cell.history.length # "1" is first access (cache miss)
      cell.history.each {|t|
        hist[t] ||= 0
        hist[t] += 1
      }
      cell = cell.next
    end
    a = 0
    hist.each_with_index {|h, i|
      h ||= 0
      a += h
      yield i, a
    }
  end

  def initialize
    @history = []
  end
  attr_reader :history
  attr_accessor :prev, :next

  def inspect
    "<#{@history.inspect}>"
  end

  def access
    if !defined?(@prev)
      Cell.register(self)
    else
      @history << Cell.age(self)
      Cell.access(self)
    end
  end
end

class Mat
  def initialize(n)
    @ary = Array.new(n) { Array.new(n) { Cell.new } }
  end

  def access(y,x)
    @ary[y][x].access
  end
end

def matmul_naive(n, a, b, c)
  n.times {|i|
    n.times {|j|
      n.times {|k|
        b.access(i,k)
        c.access(k,j)
        a.access(i,j)
      }
    }
  }
end

matmul_naive(n, a, b, c)

n = 32
a = Mat.new(n)
b = Mat.new(n)
c = Mat.new(n)
matmul(n, a, b, c)
Cell.stat_accumulation {|i, n| puts "#{i} #{n}" }

ここでは n=32 として、32*32 な行列の積をシミュレーションしている。n=32 なので、3*n**3 = 98304 がトータルなアクセス回数である。各要素の最初の一回のアクセスどうやってもキャッシュミスするのでそれを減じると 3*n**3 - 3*n**2 = 95232 がキャッシュヒットの最大となる。

素朴な行列積のキャッシュヒット

グラフを見ると階段状になっていて、3, 65, 1120 あたりで急速にキャッシュヒットが増えている。

行列全体が入らなければ c が常にキャッシュミスすると予測したが、行列全体というのは 32*32=1024 で、1120 での変化はきっとこれであろう。

65 での変化は、最内のループがまわり終わったときに b のキャッシュが一行ぶん残っているかどうかの境界であろう。

3は最内のループのひとまわりで a のキャッシュが 1要素残っているかどうかの境界であろう。

まぁ、理解はできるが、行列全体がキャッシュに入らない限り、1/3 のアクセスはキャッシュミスする、というのは悲しいわけだ。

というわけでヒルベルト曲線を使ってみよう。

def matmul_hilbert(n, a, b, c)
  nn = n.to_s(2).length
  hilbert_each(nn, 3) {|i, j, k|
    next if n <= i || n <= j || n <= k
    b.access(i,k)
    c.access(k,j)
    a.access(i,j)
  }
end

n = 32
a = Mat.new(n)
b = Mat.new(n)
c = Mat.new(n)
matmul_hilbert(n, a, b, c)
Cell.stat_accumulation {|i, n| puts "#{i} #{n}" }

素朴なのとヒルベルト曲線順な行列積のキャッシュヒット

これを見ると、行列全体がキャッシュに入らなくてもキャッシュヒットがずいぶんと増えることがわかる。

また、階段状じゃないので、キャッシュサイズを増やすとそれなりに性能があがる、という挙動が予想できる。

ただ、素朴なやりかただと、行列ひとつが入るくらい (1200程度) でキャッシュミスが最低に達するのに対し、ヒルベルト曲線順だと、行列3つがぜんぶ入るくらい (3100程度) にならないとそうならないようだ。

ところで、ヒルベルト曲線でキャッシュヒットが増えるのは、n*n*n の立方体から 8分割を繰り返して局所的に作業するからであるとすると、ヒルベルト曲線みたいに難しいことをしなくてもいいかもしれない。

たとえば、2進整数を 3bit 毎に i,j,k に割り当てていけば、8分割の繰り返しという性質は実現できる。こういうやりかたは (2次元で) Peano/Morton Curve, Quad Codes, Locational Codes, Z-ordering などといろいろな名前で呼ばれているらしい。

試してみよう。

def matmul_z_ordering(n, a, b, c)
  0.upto(2 ** (n.to_s(2).length * 3)) {|x|
    i = j = k = 0
    bit = 1
    while 0 < x
      i |= bit if x & 1 == 1; x >>= 1
      j |= bit if x & 1 == 1; x >>= 1
      k |= bit if x & 1 == 1; x >>= 1
      bit <<= 1
    end
    next if n <= i || n <= j || n <= k
    b.access(i,k)
    c.access(k,j)
    a.access(i,j)
  }
end

n = 32
a = Mat.new(n)
b = Mat.new(n)
c = Mat.new(n)
matmul_z_ordering(n, a, b, c)
Cell.stat_accumulation {|i, n| puts "#{i} #{n}" }

素朴なのとヒルベルト曲線順なのと Z-ordering な行列積のキャッシュヒット

ヒルベルト曲線よりもちょっとキャッシュヒットは少なくなる。ただ、難しいことをしなくてもこれだけのキャッシュヒットが出せれば十分だ、という考えかたはあるかもしれない。

#2

ところで、LRU なキャッシュを双方向リストで実装したわけだが、Cell を先頭に移動するのは O(1) で済むのだが、ある Cell の場所を調べるのに先頭まで線形探索をしているのが悲しい。

ここはなにか木を使うべきだろう。

とりあえず rbtree を調べて見たが、そういう操作は見当たらない気がする。

2008-03-22 (Sat)

#1

そーいえば AVL木とか、赤黒木とか、B+木とか、そういう類の木は実装したことがないな。

#2

Java ならこういうことで悩まなくていいんだろうなぁ、と思って調べて見ると、やはり TreeSet で headSet して size すれば簡単に実現できそう。

ただ、headSet でオブジェクトを生成するのはちょっと悲しいかも。

#3

3次元の Z order を可視化してみる。

3次元の Z order

ヒルベルト曲線と違って斜めに飛ぶので、きっとそのへんで局所性が減少しているのであろう。

2008-03-27 (Thu)

#1

ところで、1箇所に 1回しかアクセスしなければキャッシュは関係ないかといえばそうでもない。

メモリへのアクセスはキャッシュライン単位に行われるので、32byte とか 64byte とかの範囲での局所性も役に立つ。

たとえば、行列の転置は入力・出力の各要素に 1回ずつしかアクセスしないが、これにも Cache-oblivious algorithm がある。まぁ、縦横で長いほうを再帰的に分割していくだけだが。

2008-03-28 (Fri)

#1

日本からプラハ (PRG) への直行便はないのでパリ (CDG) 経由。

ここで、入国手続き等はどこで行われるかちょっと疑問だったのだが、結果としては、パリでセキュリティチェック・手荷物検査が行われ、プラハで入国審査であった。

ただ、チェコはシェンゲン協定を (空路において) ちょうど 30日から実施するようなので、帰りは違ったりするのだろうか。

#2

プラハ

2008-03-29 (Sat)

#1

プラハ

#2

www.google.com につないでみる。

% telnet www.google.com 80
Trying 66.249.91.103...
Connected to www.l.google.com.
Escape character is '^]'.
GET / HTTP/1.0

HTTP/1.0 302 Found
Location: http://www.google.cz/
Cache-Control: private
Set-Cookie: PREF=ID=4e7026b9ba9e16cd:TM=1206772324:LM=1206772324:S=r8u-yoSdahQ22Uq7; expires=Mon, 29-Mar-2010 06:32:04 GMT; path=/; domain=.google.com
Content-Type: text/html
Server: gws
Content-Length: 218
Date: Sat, 29 Mar 2008 06:32:04 GMT
Connection: Close

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.cz/">here</A>.
</BODY></HTML>

ここでは www.google.cz に redirect されるようだ。

2008-03-30 (Sun)

#1

プラハ

#2

突如思い立って、IO.copy_stream を Lightning Talk で発表してみる。

IO.copy_stream

2008-03-31 (Mon)

#1

プラハ

#2

夕方の便なのでちょっと観光してみる。

MUCHA museum と Prague Castle にいってみた。

#3

やはりプラハではセキュリティチェック・手荷物検査はあったが、パスポートは要求されなかった。パリでパスポートを検査された。


[latest]


田中哲