Closed grafi-tt closed 6 months ago
ビットボードの実装を眺めていて、少し最適化を試してみました。
メインの変更は一つ目のコミットで、perft を試してみて Linux / CPU Zen2 / Compiler clang18 の環境で 7% くらい高速化しました。先手の香車の効きを求めるところで、1 を下位ビット側にビットシフト複数回で伝搬させる代わりに、MSB64 を使って最上位に立っている 1 の場所を求める実装としました。
こういう処理は本質的に x86 における bsr や lzcnt 命令を使うか、ビットシフトを log(bitwidth) 回行う必要があるはずです。Zen4 より前では では bsr のコストが高いという理由からか、lzcnt 命令にコンパイルされるようでした。命令数としては bsr の方が得にはなるのでコンパイラオプションとして -mno-lzcnt つけるとか、処理をいじって lzcnt 向けに最適化するとかやってみたのですが、速くなる感じはなかったので、コードのシンプルさなども考えて今の比較的わかりやすい書き方にしました。
-mno-lzcnt
AgnerFog https://www.agner.org/optimize/instruction_tables.pdf を見る限り AMD 系だと lzcnt のレイテンシが 1 で、Intel 系だと 3 のようなので、Intel 系では僅かにしか速くなっていないとは思います。
具体的には
// old mocc |= mocc >> 1; mocc |= mocc >> 2; mocc |= mocc >> 4; mocc >>= 1;
と
// new // `MSB64(x`) is `lzcnt(x) ^ 63` or `bsr(x)` mocc = ~uint64_t{0} << MSB64(mocc | 1);
の比較ということになりますが、前者のレイテンシは 7 cycles で、後者のレイテンシは Intel だと lzcnt で 6 cycles、bsr で 5 cycles となります。
また、128bit のデクリメントの最適化もやってみました。さらに unpack をなくしてみるのも試したのですが、やっぱり unpack した状態でデクリメントする現状の実装のほうが速そうなので諦めました。
プルリクありがとうございます。素晴らしいです!
当該部分の香の利きのコード、私は書いてる時は「このあと55将棋と56将棋の対応コード書かないといけないから、わかりやすいコードにしよう…」と思って詰めきれてませんでした。
ビットボードの実装を眺めていて、少し最適化を試してみました。
メインの変更は一つ目のコミットで、perft を試してみて Linux / CPU Zen2 / Compiler clang18 の環境で 7% くらい高速化しました。先手の香車の効きを求めるところで、1 を下位ビット側にビットシフト複数回で伝搬させる代わりに、MSB64 を使って最上位に立っている 1 の場所を求める実装としました。
こういう処理は本質的に x86 における bsr や lzcnt 命令を使うか、ビットシフトを log(bitwidth) 回行う必要があるはずです。Zen4 より前では では bsr のコストが高いという理由からか、lzcnt 命令にコンパイルされるようでした。命令数としては bsr の方が得にはなるのでコンパイラオプションとして
-mno-lzcnt
つけるとか、処理をいじって lzcnt 向けに最適化するとかやってみたのですが、速くなる感じはなかったので、コードのシンプルさなども考えて今の比較的わかりやすい書き方にしました。AgnerFog https://www.agner.org/optimize/instruction_tables.pdf を見る限り AMD 系だと lzcnt のレイテンシが 1 で、Intel 系だと 3 のようなので、Intel 系では僅かにしか速くなっていないとは思います。
具体的には
と
の比較ということになりますが、前者のレイテンシは 7 cycles で、後者のレイテンシは Intel だと lzcnt で 6 cycles、bsr で 5 cycles となります。
また、128bit のデクリメントの最適化もやってみました。さらに unpack をなくしてみるのも試したのですが、やっぱり unpack した状態でデクリメントする現状の実装のほうが速そうなので諦めました。