Bonanzaのbitboardのbit layout
■ Bonanzaのbitboardのbit layout
Bonanzaでは局面をbitboardで持っていることは前回触れた。
しかし問題となるのは、このbit layoutである。典型的なbitboardに対するオペレーションとして、次のようなことをしたい。
bitboard bb; i = 0; foreach_bit (sq in bb) koma[i++] = square[sq];
上の疑似コードは、bitboardから1の立っているビットを1ビットずつ取り出して、そのビット位置をsqに代入し、ループを回るという意味である。
指し手生成などでこの手のループを書きたいのは自然なことである。
MSB(最上位ビット)/LSB(最下位ビット)から順に1である最初のbitを見つけるのは、x86/x64系ではbsr/bsf(bit scan reverse/bit scan forward)という命令がある。当然、これを使わなければならない。さもなくばお話にならない。
MSCなら、_BitScanReverse(MSBから)、_BitScanForward(LSBから)というマクロ(?)を用いれば、bsr/bsfに展開されることが保証されているので、これを用いる。
ただし、bsr/bsfは、最初に1になっているbitを見つけ出すだけであり、そのあとそのビットをクリアしてくれるわけではない。よって、次のように書く必要がある。
bb = BB_BPAWN; while ( BBToU(bb) ) { sq = FirstOne( bb ); Xor( sq, bb ); list0[nlist] = f_pawn + sq; list2[n2] = e_pawn + Inv(sq); score += kkp[sq_bk0][sq_wk0][ kkp_pawn + sq ]; nlist += 1; n2 += 1; }
上のソースは、Bonanzaのevaluate.cの一部である。BBToUはbitboard全体が0かどうかを判定している。これはマクロで、単に内部で保持している3つのdwordのbitwise ORをとっているだけである。
FirstOneもマクロであり、実際はfirst_one012が呼び出される。FirstOneは、MSBからscanして最初に1であるbitを見つける。
その直後にXorしているが、これもマクロで、一見するとbitboard型同士のXorに見えるが、よく見るとsqはbitboard型ではなく整数型であり、指定した位置のbitをXorするものである。しかし、そのbitはすでに1なのでそれとXorすれば、そのビットのみがクリアされる。
要するに、BBToU〜FirstOne〜Xorというコンボによって、先ほどの疑似コードとして示した foreach_bit (sq in bb) が達成できているということである。
FirstOneで検索したときについでにそのビットを0にしてしまえば、わざわざbitboard(12バイトもある)とxor用のテーブル(12バイト)をlookupしてxorをする手間が省ける。
これにより、数命令ほどはしょれて思考ルーチンを0.5%ほど高速化できる。まあ、細かいチューニングの話はいったん置いておこう。
bitboardは3つのdwordから成る合計12バイトの構造体である。8×12 = 96bitある。将棋盤は81枡だから、15bit余る。ではどこを余らせるのかという問題がある。
常識的には、上位15bitを余らせて、下位81bitだけ使ったほうがいいように思う。64bit OSで動かしたときに、64bit型に対するbsr + 32bit型に対するbsrというように2回のbsrで動作を完了させることが出来て少しお得だからである。
Bonanzaではどうなっているだろうか。ソースを読んでいこう。
int first_one012( unsigned int u0, unsigned int u1, unsigned int u2 ) { unsigned long index; if ( _BitScanReverse( &index, u0 ) ) { return 26 - index; } if ( _BitScanReverse( &index, u1 ) ) { return 53 - index; } _BitScanReverse( &index, u2 ); return 80 - index; }
このfirst_one012は、bitop.cで書かれている。(何故、inline化されるようにbitop.hに書かないのかは知らないが。ちなみにbitop.hにbitop.cで書かれている関数を移動させると思考ルーチンが0.5%ほど高速化できる。)
見ての通り、offsetとして、26引いてあることからもわかるように、盤面の3段ごとにdwordひとつである。(3×9 - 1 = 26)
つまり、それぞれのdword変数の上位32-27 = 5bitずつは余らせてある。
また、first_one123の返す値は、
・ひとつ目のdword変数の26bit目が1のときに0
・ひとつ目のdword変数の0bit目が1のときは26
・ふたつ目のdword変数の26bit目が1のときは27
・ふたつ目のdword変数の0bit目が1のときは53
・みっつ目のdword変数の26bit目が1のときは54
・みっつ目のdword変数の0bit目が1のときは80
となっている。
上のfirst_one123の実行フローを見れば、first_one123は、二つ以上のbitが立っている場合は、小さなほうの値を返すことがわかる。
では、ここで返された値が盤上でどの位置を意味するのだろうか。
それは、shogi.hにある次のenumのA9〜I1がそれである。コメントは私が付与した。
// 各マスに対する文字列 // 上が後手側(A9 = 9一 , I1 = 1九) enum { A9 = 0, B9, C9, D9, E9, F9, G9, H9, I9, A8, B8, C8, D8, E8, F8, G8, H8, I8, A7, B7, C7, D7, E7, F7, G7, H7, I7, // x < A6の範囲が後手陣。先手にとって駒が成れるのはこの範囲。 A6, B6, C6, D6, E6, F6, G6, H6, I6, A5, B5, C5, D5, E5, F5, G5, H5, I5, A4, B4, C4, D4, E4, F4, G4, H4, I4, // x > I4の範囲が先手陣。後手にとって駒が成れるのはこの範囲。 A3, B3, C3, D3, E3, F3, G3, H3, I3, A2, B2, C2, D2, E2, F2, G2, H2, I2, A1, B1, C1, D1, E1, F1, G1, H1, I1 }; // FirstOneは小さな値から探すので、この盤面で言えば左上からのスキャンになる。 // LastOneは大きな値から探すので、この盤面で言えば右上からのスキャンになる。
I1が(将棋盤の)1九の位置。
よって、bitboardの3つのdword変数のうち、
・1つ目のdword変数の26bit目が1なら、FirstOneは0を返し、これはA9を意味して、A9は将棋盤上の9一の地点である。
・1つ目のdword変数の0bit目が1なら、FirstOneは26を返し、これはI7を意味して、I7は将棋盤上の1三の地点である。
・2つ目のdword変数の26bit目が1なら、FirstOneは27を返し、これはA6を意味して、A6は将棋盤上の9四の地点である。
…
・3つ目のdword変数の26bit目が1なら、FirstOneは80を返し、これはI1を意味して、I1は将棋盤上の1九の地点である。
■ まとめ
Bonanzaのbitboardのbit layoutがどうなっているのかは意外と知られていない。今回は、bit layoutについて詳しく調べた。
3段ごとにdwordをひとつ割り当てるのは、コーディングは容易になるかも知れないが、64bit OSで動かすなら、変にpaddingがされているのは速度的に損だと思う。(Bonanzaのbitboardまわりは、64bit環境で動作させることを前提にして、かつソースを何ヶ所か変形するだけで、思考ルーチンを10%程度高速化できることが私にはわかっている。)
Bonanzaのソース上には、64bit用に特化したようなコードが存在しないが(例えばbitop.hは64bit変数で書き換えれば64bit環境で高速化が期待できる)、それは64bit環境が普及する前に書かれたコードなのである意味仕方がないのかも知れない。
新規にbitboardを使ってコンピュータ将棋プログラムを書こうという人は、Bonanzaのbitboardの処理は大いに参考になると思うが、Bonanzaのbitboardのbit layoutまでは真似しないほうがいいと思う。