期待が高過ぎたみたい
前回の記事で3三将棋アプリ
のソースを元にC++
に書き換えた話を書きましたが、出来上がったC++製
のプログラムはCoffeeScript(JavaScript)製
のものより遅い結果となりました。当ブログで度々「C++
にすればもっと深く読めるのに〜」なんて書いてきましたが、C++
に対する期待が高過ぎたようです。たとえC++
にとって不利なデータ構造であっても、C++
にはそんなものでは超えられないほどの実行速度の優位性があると信じていたのですが、認識が間違っていたようです。
実行速度を比較する際は、実際に作ってみないと正確には分からないという点が辛いところですが、今回はGrok4
にほぼ丸投げで試すことが出来たので、その点は助かりました。
C++版がどれぐらい遅いのか
CoffeeScriptの3三将棋アプリのレポジトリとC++版のリポジトリのREADME.mdファイル
に、先手・後手双方をAI同士で対戦させた実行例を記載していますが、CoffeeScript
で1781ミリ秒
で終了する対局がC++
では倍以上の4434ミリ秒
掛かっています。興味のある方は開発環境を用意すれば再現できます。
ChatGPT(GPT-5)
が提案するコンパイルオプションもいろいろ試しましたが、ほとんど効果はありませんでした。
ChatGPT(GPT-5)によるアドバイス
以下の質問から始めて、ChatGPT(GPT-5)
にいろいろアドバイスを求めました。
同じアルゴリズムとデータ構造で作ったJavascriptとC++のゲームプログラムがあって、Javascript製の方が実行速度が速いとき、どのような原因が考えられますか?
会話の中でChatGPT(GPT-5)
に言われたことをいくつか箇条書きにしてみます。
- 「同じアルゴリズム・データ構造で作ったゲームプログラムで、C++よりJavaScriptの方が速い」というのは直感に反しますが、実際に起こり得ます。
- なるほど!Piece という抽象クラスがあって、それを継承した Pawn, Rook, Bishop … みたいな形で設計されているわけですね。これはオブジェクト指向的にきれいなんですが、将棋AIの「思考部分(探索+評価)」ではかなり不利です。
- なるほど…😅将棋AIをオブジェクト指向で作っている場合、「設計としてはきれい」だけど「パフォーマンス的には厳しい」という典型的な状況にハマっていますね。
- 今の実装は「オブジェクト指向的にはきれい」だけど、性能的には JS エンジンの hidden class 最適化に負けているのが原因です。抽象クラスを廃止して struct+enum にするだけで、C++ の方が確実に速くなるはずです。
- ここが C++ 版が JS に負けている一番の原因候補です。JS のオブジェクトは 実際には JIT によって連続配列のように最適化されます。一方 C++ の「駒ごとのオブジェクト集合+ポインタ参照」はメモリがバラバラに配置される、毎回ポインタを間接参照する、キャッシュミスが頻発するので「CPU が待ち時間ばかり」で全然速くならない、という現象が起こります。
実際、将棋エンジンやチェスエンジンは 必ず「盤面を配列で表現」しています。
巷に出回っている将棋ソフトの真似ではなく自分でオプジェクト志向
で作ってみたいと思って作り始めた(元々はRuby
で作ってました)ので、ChatGPT(GPT-5)
に指摘された内容はある程度認識していましたが、C++
のようなコンバイラ言語ならCoffeeScript(JavaScript)
より遅くなることはないだろう、なんて勝手に思っていました。それにJS エンジンの hidden class 最適化なんてことは、ChatGPT(GPT-5)
に指摘されるまで知りませんでした。JavaScript
って自分が思っていたより元々速いんですね。
そう言えば同じアルゴリズムとデータ構造で昔作ったRuby
製の将棋ソフトよりJS
の方が断然速いなぁとは感じてました。
小さなリファクタリング
ChatGPT(GPT-5)
に、大きなリファクタリングが大変なら以下のような高速化案がありますと言われました。
- vtableを極力通さない
もし 駒種はコンストラクタで固定なら、Piece に「enum型のメンバー」を持たせておけば仮想関数を呼ばずに済みます。→ piece->type に直接アクセスできれば、仮想関数呼び出しを消せる。 - 駒オブジェクトをヒープでなく配列に置く
new してポインタ管理するのではなく、最初にstatic Piece allPieces[MAX_PIECES];を作って Board はポインタではなく インデックス(int)で参照する。
これなら 全駒が連続配置されるのでキャッシュ効率が改善します。 - 評価関数の高速化
もし各駒に evaluate() があるなら呼ばずに、 switch(piece->type) あるいは pieceSquareTable の配列参照に置き換えるだけでもだいぶ速くなるはず。 - コンパイルオプションで粘る
既に -O2 をお使いですが、
g++ -O3 -march=native -flto=8 -funroll-loops -DNDEBUG
を試すと、関数インライン化やループ展開が強化されて多少マシになります。
簡単に試せそうなのでソース全体を通して1.のChatGPT(GPT-5)
からのアドバイスを実装してみました(koma->getKind()で駒種を取得していた処理をやめて、publicなenum型のメンバ変数を追加してkoma->typeで駒種を判定)が、4434ミリ秒
掛かっていた処理が400m秒
程短縮出来ただけで全然CoffeeScript(JavaScript)
には敵いません。
ということで、今の将棋・チェス関連アプリはJS
のまま置いておくのが一番良いのかなと思っていて、改善するにしても今のCoffeeScript
のまま改善していく方向で考えています。
それでもOOPが好き
OOP(オブジェクト指向プログラミング)
を絶対視する気はありませんが、OOP言語
を使う限りはOOP
すべきなんじゃない?なんて思ってましたが、実効性を重視するなら拘らないほうがいいのかもしれません。昔、Rails
のプロジェクトでありながらORM(O/Rマッパー)
を使わずにSQL文
を書いているなんてプロジェクトの話を聞いたことがありますし、もしかしてGo言語
やRust
など非OOP言語
が勢力を伸ばして来ているのは、よく知りませんが、実行速度を上げること(PCが処理しやすい)を重視する流れなのかもしれません。今後プログラムは人間が書くものではなくAIが書くもの?という流れの一貫で、人間側に寄せるのか?プログラムを実行するコンピュータ側に寄せるのか?という話なのかもしれません。
でもRails
が出てきた頃にORM
に感動した自分としては、Rails
を使いながらSQL文
を書くなんていう本末転倒なプロジェクトには参加したくありませんし、今後もOOP
で作りたいと思ってます。「OOPとは何か?」
なんて話をすると主語が大きくなりすぎて自分には語れませんが、直感的に人間が理解しやすいと思うんですよねぇ。今回のケースもOOP
が悪いということではなく、まだまだ改善の余地があると思ってます。というか、取り敢えず機能を実現することを優先していて、高速化に関しては深く考えていませんでした。後はC++
にすれば自然と速くなるだろう、なんて考えながらも手を付けられずに長い間放置していた感じです。
新規にGUI
から作ることを考えれば現状のCoffeeScript
のリファクタリング&高速化に着手するほうが楽ですし、どれ程の結果が出るか未知数ですがいつくかアイデアもあるので試してみたいと思ってます。