Open20

将棋エンジンを作る、P2P通信対戦をする、対戦AIを作る

sh11235sh11235

Claude Codeを使ってどこまで複雑なアプリを自分が作れるかのログ。
2025年9月にはCodex CLIも仲間入りした。

使っている技術

  • turbo(Node.jsのモノレポ構成)
    • web: UIの代表例
      • React: UIコンポーネント
      • Zustand: 状態管理
      • WebRTC: P2Pをサーバレスで実現してwebブラウザでの対戦を実装
    • core: ゲームルールを置く(全部Rustに置き換え中)
      • Node.js
    • rust-core: ゲームルール、対局時間管理、USI対応
      • wasm-bindgen
        webなどで使うためのWASMを生成する
      • AI
        初期はNode.jsで書いていたがパフォーマンスを出せる期待のある & デスクトップ版への展開も出来るRustで書くことにする(Electronを使えばNode.jsでもいい、要するに好み)
        エンジン部分が(主に探索の)パフォーマンスが欲しいのであって、デスクトップアプリレイヤーはそこまでパフォーマンスセンシティブでは無さそう。

最新状況

https://github.com/SH11235/shogi

初期に作った超弱い対戦CPUがある。Rustで書いているエンジンとの統合がまだなので弱いまま。
https://sh11235.github.io/shogi/
このページは近日中に全置換えされる予定。

sh11235sh11235

生成AIでvibe codingってのをやってみたい

将棋を最近覚えたのでアプリを作ってみたくなった。
現代は生成AIが0→1の実装は得意なのでとりあえず動くものは出来るだろうと期待。
オセロ(reversi)は簡単すぎて学習済のデータでプロンプト1発で出来てしまいそうだが、将棋くらいだとそこそこ人間も頑張らないといけないとだろうなと思いながら着手。
webの人間なので最初はwebアプリを目指すが、ある程度色んな環境で使いまわせるようにcoreロジックとUIを分けて作ることを意識して生成AIに指示を出して進めた。
最初はChatGPTと一緒に要件定義をふわっとやってGitHub isseusを作成して、Codexでタスクをやらせるというのを試みた。
が、あんまりCodexがやるタスクのクオリティに満足できず、しばらくしてClaude CodeのMAXプランを契約した。ここからコーディング(の依頼)がとてもやりがいあるものになった。
数日である程度は動くwebアプリが出来た。
GitHub Pagesにdeployすることにした。
https://github.com/SH11235/shogi/commit/e78371afe3bd1605f630277fb576b8881d2d1991

sh11235sh11235

通信対戦したい

それらしいアプリはできたものの、将棋の知らないルール(特殊勝利条件などや、打ち歩詰めなどの禁止された手)がそこそこあり、調べつつ実装した。
1人でボードを操作できるようになったが、将棋は1対1の対戦ゲームなので、自然な発想として通信対戦をしたくなる。
zennの記事で
https://zenn.dev/yadokani389/articles/matchbox-iroh-20250529
を見て久しかったのでこれを参考に、シグナリングサーバー無しでのP2P通信を実現したいと考えた。
今のアプリではゲームエンジンは使っていないので、Bevy前提のcrateは使わずに進められるかをClaude Codeと考えた。
zenn記事を読ませたり、記事中のGitHubレポジトリをgit cloneしてきてソースコードを読ませたりして、指示だけでP2P通信を実装させようとしたが難航した。
いきなり将棋の通信対戦機能を作りたいと依頼してもうまくいくわけもないので、単なるテキストのやり取りを出来るChat機能だけを実現するのをゴールとして初期実装をした。
紆余曲折があったが、参考記事のcrateは使わずClaude CodeのContextからは忘れてもらって、WebRTCを使うという方針で実装方針を立てなおしてとりあえず動くものが出来た。
最初に出来たのが以下(Reactコンポーネントでもなく、単なるhtmlにRustでbuildしたWASMのWebRTC機能を読ませたもの)。
https://sh11235.github.io/shogi/webrtc-demo.html

これが動いたとき、こんな簡単にP2P通信できるんだっていう感動があった。
いい時代になった。

紆余曲折

これが出来るまでにClaude Codeが作業をよく投げだす事態にあった。
P2P通信が出来るようになった、との報告があり、確かにブラウザを2つ起動してコネクションを確立しているが、実際はシグナリングサーバーをlocalで立てている、という実装を提出された。
作業報告の最後の注釈で、「シグナリングサーバーをlocalで立ててそれ経由で通信できます、TODOとして真のP2Pを目指します」と書かれていた。
これを複数回繰り返し、結局サーバレスP2Pを諦めて同じ方針に落ち着き続ける挙動が見られたので、contextをリセットして要件をまっさらな状態で一緒に考えるという文脈を作ることで上記のものが出来た。
途中で libp2p というcrateを生成AIが見つけて使おうとしたがやめた。
結局 web-sys(JavaScriptラッパーで、大体のJavaScriptのAPIを呼べる理解をしている)でWebRTCを書いた。

これをベースに、将棋アプリで通信対戦が出来るようになったのが以下の変更。
https://github.com/SH11235/shogi/pull/24

参考記事がRustだったのでRustでWebRTCの実装を持っていてJavaScriptでWASMをロードして使うという実装になっているが、この時点のコードだとRustを介在させず純粋にJavaScript(TypeScript)でWebRTCを書いても良かったが、Rustで書かれていると無条件の満足感が生まれるのでよしとした。
友達に触ってもらって確かに通信対戦できることを確認できて満足した。
触っていると気づくUI不具合やバグは色々出た。
サーバーを絶対の状態のstoreと出来ないので、お互いのclientの情報で乖離が出たとき(盤面リセットを片方のプレイヤーだけが実施する等)には正常には進行不可能な状態になるなど。
client間で状態の整合性を取り続ける工夫を入れる必要があるが、今はそこの品質はいいかなということで深入りはしていない。

sh11235sh11235

開発スタイル

プロンプト

ある程度アプリが出来てコードベースが大きくなる & タスクの応用度が上がってくると、的外れな生成をすることが目立つようになった。
よく言われているplanモードで実装計画を立ててから実装させるというのが本当によく効いた。
むしろこうしないと何もうまくいかない。
また、実装計画を立ててそのまま実装、だと作業途中でコンテキストウィンドウがいっぱいになって、 /compact でコンテキストを圧縮した後に露骨に作業品質が落ちるのが見られる。
複雑なタスクをやらせつつ、コンテキストウィンドウの問題を何とかするために、実装計画をドキュメントファイルとして吐き出させて、実装時にはドキュメントを参照させて必要分のコンテキストを補うという方法をとるようになった。
また、TDDで進めないと作業チェックが最後の最後のユーザーサイドでの結合テストだけになってしまうので、実装計画を立てるタイミングでタスクの細分化と、テストケースを考えることもセットで行った。

ある程度繰り返すと使い捨てでいい実装計画(詳細が書かれすぎてそのうちすぐに実装との乖離が出る)ドキュメントが出てくるので、こいつらをどう処理するか(消してしまうか、残しておくか)で少し悩んだ。
一旦はarchiveディレクトリに移してgit管理はしておく、ということにした。

CI

タイプチェック、format、lint、自動テストはだいぶ早いうちに導入した。
これはやっておいてよかったというか、やっていなかったら一瞬でコードベースが破綻していたと思う。

MCP

PlaywrightのMCPを登録していると指示をしたときや時折自主的にブラウザ操作を自分でやってくれる。
上手くいくこともあればいかないこともあって、大体はうまくいかない。
consoleにログを流すコードを書いて自分で確認するというのは部分的に小さく成功することもある。
Windows WSL環境で開発をしているが、ちょっと前(2020年くらい?)はWSLとホストの連携周りのトラブルやGUIアプリをWSLで動かすのに難があったが、今のWSL2、Ubuntu環境では特に設定なくPlaywrightでブラウザ操作していてこれはこれで感動した。
いい時代になった。

sh11235sh11235

対戦AIを作る

Claude Codeに雑に作らせるととても雑な対戦AIは出来た。
将棋は詳しくないが違和感のある手を普通に打ってくる。
例えば人間が先手で以下の進行が発生する。
26歩→33歩(ここは自然)→25歩→55角
これが強い手なのか、自分には将棋の感覚が備わっていないが、将棋の対局をYoutubeで見るには見たことが無い進行ではあるし、実際自分が適当に差しても勝ててしまう。
世の中にフリーで遊べる将棋のサイトのAIの方が何倍も強い。
それはそうと、勝手に手を指してくれるAIが出来たのはとても嬉しい。
ここまでの開発はP2Pで少し停滞したのと、細かい将棋のルールを抑えるのにちまちま時間を使ったくらいで、概ね作業はずっと前に進んでいた。

sh11235sh11235

定跡を搭載

今まさに詰まっている。
簡単な序盤の定跡をJSONファイルなどで手で(生成AIに手伝ってもらいつつ)作って対戦AIに読ませる、という事自体は小さく成功したが、これだけでは満足できない。
将棋エンジンを調べてみた。
一番有名そうなのが
https://github.com/yaneurao/YaneuraOu
であり、ここの方がなんと定跡ファイルを公開されている!(MITライセンス)
ということでここのYaneuraOu形式の定跡ファイルをなんとか取り込めないかという試みを始めた。

課題

定跡ファイルが470 MBある。
Webアプリでこの要領のファイルをアプリで読ませてきびきび動かすことは現実的ではないと思う。
ここでフロントエンドのパフォーマンスを最適化するための施策を打つ必要が出てきた。
やりたいことは

  • 定跡を対戦AIに搭載できるように実装を拡張する
  • YaneuraOuさんの定跡ファイルのパーサーを作る
  • 自前のアプリで使える形式に圧縮する

である。
パーサーを作ることについてはCLIツールでよいのでスクリプトをClaude Codeに書かせようとするとPythonで書き始めたので、Node.jsで書くように指示しなおした。
しかしNode.jsで作ったファイル操作スクリプトを実行するとclaudeの端末が落ちることが頻発した。
heap limitのエラーが出ていて、最初はNode.jsのスクリプトが悪いのかと思っていたが、claude自体がnpmパッケージで配布されているようにNode.jsで動いているのでclaudeのプロセス内で大きなファイルを扱うプロセスを呼ぶとメモリがいっぱいになってしまうらしい(?)。
対応としては人間がファイル操作スクリプトの実行をする、もしくはプロンプトで定跡ファイルを使ったスクリプトを実行するときはファイルサイズに気を付けて一部だけ読んでスクリプトを動かす、などの指示をした。
Node.jsで色々試行錯誤したが(自分が定跡ファイルのファイル形式をちゃんと見ず、また知識が無かったのもあって)ろくなものが作れず、Claude Codeが出来たと言っている生成物を読ませる対戦AIの実装は全く機能しなかった。
コードはだめだが、アプローチとしてWebWorkerを使うことと、一気に大きなファイルを読み込ませるのではなく、定跡ファイルを分割して少しずつバックグラウンドで読ませていく、というアイデアだけは抽出できた。
今までで一番大きな詰まり方をした。
コードベースは単発スクリプトや生成データ、それを読み込むアプリの実装で散らかり、リトライするたびに発散する一方である。
CLIツールのパフォーマンスが気になったのでNode.jsではなくRustでCLIを作り出し、パフォーマンスは確かによくなったが正しい処理を出来ているのかちゃんと判断できない、Node.jsの残存コードが残ったまま、など新たな問題を産んだ。

ここで一度立ち止まってちゃんとソースコードを自分で把握し、発散しきったものを整理することにした。

sh11235sh11235

ファイル整理

大体整理した
区切りがついた時点のcommit
https://github.com/SH11235/shogi/commit/a7680ad97e2072932da4d7bf930364762655aa78

  • 単発開発用の一時ドキュメントをarchiveに移す
  • 未参照コード・ファイルを消す
  • 呼び出してはいるが不要な処理を消す
  • TODOのように実装途中でそれらしくごまかしている箇所を消す
  • TypeScriptのclass構文をなくす
    などなど

CLIツールはNode.jsではなくRustにしたことで実行時パフォーマンスが明らかによくなったので嬉しい

sh11235sh11235

将棋エンジン調査

どうも、チェスのエンジンの実装を参考に将棋用に作ったエンジンも多いらしい。
確かにボードゲームとして将棋とチェスはそこそこ似通っているので、対戦AIの作り方で同じ考え方が通じるところもあるのだろう。
C++製が多い。

YaneuraOu

https://github.com/yaneurao/YaneuraOu
多分国内で一番有名どころ?
将棋AIの水匠が公開されていてこれを使って遊ぶYoutube動画をしばしばみる。
定跡ファイルを公開されていて、界隈の研究の底上げにも貢献されていそう。

Fairy-Stockfish

https://github.com/fairy-stockfish/Fairy-Stockfish

Apery

C++版 もあるが、こちらは開発終了していて、Rust版に移行しているらしい。
https://github.com/HiraokaTakuya/apery_rust

  • Rust製
  • アルファベータ探索アルゴリズム
  • KPPT(King-Piece-Piece-Turn)評価関数
  • マルチスレッド並列探索

stoat

https://github.com/Ciekce/stoat
比較的最近作られた将棋エンジン。
chessのエンジンを作っている方が将棋のエンジンも作り始めているみたい。

dobutsu

https://github.com/clausecker/dobutsu
動物将棋?というおそらく将棋ライクなゲームのエンジン。

tenuki-d

https://github.com/hikaen2/tenuki-d
将棋AIのTanukiを作っている方?
Tanukiをもじったネーミング。

  • Dart製

JapaneseChess

https://github.com/kimrin/JapaneseChess
将棋は海外だと JapaneseChess と呼ぶのだろうか。
Juliaで書かれている。

This Japanese Chess Engine works with the "Shogi Dokoro" Shogi UI frontend system.

と記載があり、 Shogi DokoroというUIシステムが世の中に知られているらしい。

bonanza

https://github.com/ianfab/bonanza

Mar 17, 2025にpublic archiveになっている。
名前を聞いたことがあるので、当時(~2018年?)有名だったのであろう将棋AIと思われる。

cshogi

https://github.com/TadaoYamaoka/cshogi
Pythonライブラリと書かれているので、将棋エンジン本体というよりは開発者・ユーザー向けの補助ツールの側面が強いのかな。
Aperyの名前が出てきて、一部はAperyを流用しているとのこと。
やねうら王、Aperyが日本国内だと有名なのかな。

python-dlshogi2

https://github.com/TadaoYamaoka/python-dlshogi2
無印のpython-dlshogiもあった。
cshogiと同じ作者の将棋エンジン。

DeepLearningShogi

https://github.com/TadaoYamaoka/DeepLearningShogi
これがdlshogiと呼ばれているもの?

hachu

https://github.com/ddugovic/hachu

将棋だけでなく大、中、小(漢字は多分これ)将棋のAIエンジンらしい。
将棋派生のゲームなら同じ感じでAIエンジンを作れるだろうのでまとめて作っているのか。
チェスおよびチェス派生にも対応しているらしい。

nshogi-engine

https://github.com/nyashiki/nshogi-engine
2024年に作られ、最近も開発が続いていそうなエンジン。
Cudaのコードがある。

Topics
reinforcement-learning monte-carlo-tree-search shogi alphazero gumbel-alphazero

とそれらしい単語が並んでいて、alphazeroというのが囲碁のエンジン?らしい。
将棋より確か先に囲碁の方が対戦AIが人間より強くなったんだったか。

thunderlight-shogi

https://github.com/thunderlight-shogi/engine

ここにあるリンクはどうもアクティブではないが、一時期特定のプラットフォーム上で将棋が遊べるサイトがあり、そのソースコードがOSSになっているらしい。
自分が作り切ろうとしている範囲はこのOSSくらいのものかなという気がしている(時間と好奇心のどちらかが途絶えたら終わりの予定)。

sh11235sh11235

座標

調べものをしたり実装を見たりしていて色々な座標系があって混乱したので、ChatGPTに表を作ってもらった。
ここの理解が間違っていると、コードを書くときにの脳内将棋盤座標のマッピングがずれて正しい機能実装およびテストが書けない。

将棋座標 4 方式の早見表

視点 / 用途 表記フォーマット 筋 (file) の並び 段 (rank) の並び 例:中央の「3四」
日本式
(棋譜・現実盤)
数字(1-9) + 漢字(一-九) 右端 1 ➜ 左端 9 上段 ➜ 下段 3四
USI
(プロトコル着手)
数字(1-9) + 英字(a-i) 右端 1 ➜ 左端 9 上段 a ➜ 下段 i 3c
SFEN 盤面文字列
(FEN 互換)
1段=9 文字を / 区切りで 9段並べる 左端 9 ➜ 右端 1
(数字は逆向き)
上段 (一段) ➜ 下段 (九段) 1行目 7文字目
(行=2, 列=2)
BitBoard index
(行優先 0–80 の代表例)
整数 0 … 80 左端 9 ➜ 右端 1 上段 ➜ 下段 38
index = (rank-1)*9 + (9-file)

代表マスの具体例

日本式 USI SFEN (row,col) BitBoard
1一 1a (0, 8) 8
2八 2h (7, 7) 70
5五 5e (4, 4) 40
7七 7g (6, 2) 56
9九 9i (8, 0) 72
sh11235sh11235

SIMD対応でのトラブルシューティング

SIMD(Single Instruction, Multiple Data)

ChatGPT o3のSIMDについての解説

SIMD(Single Instruction, Multiple Data)最適化とは
一つの命令で複数データを同時に処理し、演算の並列度を上げて性能を引き出す手法
― これをアプリケーション側・コンパイラ側の工夫で実現することが「SIMD最適化」です。

  1. 背景となる CPU の仕組み
    用語 概要 代表的な ISA
    スカラ命令 1 命令 = 1 データ x86 の ADD rax, rbx など
    SIMD 命令 1 命令 = N データ(同種演算を並列) AVX2 (256bit)、AVX-512 (512bit)、NEON (128bit) など

例:AVX2 vaddps ymm0, ymm1, ymm2 は 256bit = 32bit×8 個の浮動小数点を一度に加算

CPU には ベクタレジスタ(ymm0、zmm0 …)があり、幅が広いほど一命令で扱えるデータ量が増える

  1. なぜ速くなるのか
    演算器を水平展開

同じ演算を 4〜16 倍の並列度で実行(命令数と分岐が減少)

メモリ帯域が有効活用される

連続データを一括ロード/ストアすることでキャッシュヒット率が高まる

命令デコーダの負荷が軽くなる

命令本数そのものが減り、フロントエンドのボトルネックが緩和

  1. SIMD 最適化のアプローチ
    レイヤ 方法 Rust での例 注意点
    自動ベクトル化 コンパイラがループを解析して SIMD 命令を生成 rustc -C target-cpu=native -C opt-level=3
    LLVM が自動判定 分岐が入る・メモリアラインが悪いと崩れる
    明示的インストリンシック CPU 固有命令を直接呼び出し core::arch::x86_64::_mm256_add_ps 可読性・移植性とバーター
    高水準 SIMD API ポータブルな抽象レイヤ 1.70 以降の標準 std::simd::*
    packed_simd_2, wide, simdeez など コンパイラ対応・安定度を確認
    ランタイム分岐 起動時に CPU 機能検出 → 最適版を選択 cpufeatures, is_x86_feature_detected!("avx2") ビルドフラグ target-feature と組合せ

  2. 実践での最適化ステップ
    ホットスポット計測

perf, cargo flamegraph で重いループを特定

メモリ連続性を確保

Vec<T> / スライスを AoS→SoA に再構成することも

自動ベクトル化をまず試す

インライン化、制御フロー削減、#[inline]、#[repr(align(32))] など

インストリンシック or std::simd へ差し替え

性能を測りつつ段階的に(AVX-512 は AVX2 よりレーン数が多いがクロック低下にも注意)

CPU フィーチャ検出でフォールバック

旧世代 CPU や ARM Mac でも動くように

  1. 注意すべき落とし穴
    アラインメント違反アクセス → セグフォ or パフォーマンス低下

loadu 命令なら動くが遅い。可能なら 16/32/64 バイト境界にそろえる

分岐の多いアルゴリズム → ベクトル化に不向き

分岐をマスク演算に変換できるか検討

メモリ帯域の飽和 → 演算よりメモリがボトルネックなら恩恵が薄い

クロックダウン(Intel AVX-512 など)

高度最適化ほど「省エネ/熱制約」の影響が大きい

  1. Rust での簡単な例(AVX2 256bit 加算)
#![feature(portable_simd)]

use std::simd::{f32x8, SimdFloat};

pub fn add_arrays(a: &[f32], b: &[f32], out: &mut [f32]) {
    assert_eq!(a.len(), b.len());
    assert_eq!(a.len(), out.len());

    let chunks = a.len() / 8;
    for i in 0..chunks {
        let idx = i * 8;
        // 安全:インバウンドかつ 8 要素境界
        let va = f32x8::from_slice(&a[idx..]);
        let vb = f32x8::from_slice(&b[idx..]);
        let vc = va + vb;
        vc.write_to_slice(&mut out[idx..]);
    }
    // 余り要素はスカラで処理
    for i in (chunks * 8)..a.len() {
        out[i] = a[i] + b[i];
    }
}

std::simd はプラットフォームに応じて AVX2 / NEON などへ自動マップ

RUSTFLAGS="-C target-cpu=native" で最適コード生成を促進

まとめ
SIMD 最適化は「同一演算をまとめて処理 → 演算器を並列利用して高速化」するテクニック

まずは 自動ベクトル化 + プロファイル計測、必要に応じて 手動 SIMD を導入

Rust なら std::simd や インストリンシック、CPU フィーチャ検出クレートを活用すると保守性と性能のバランスが取りやすい

これらを意識してコードを設計・リファクタリングすると、NNUE 評価関数のような大量並列アルゴリズムでも レイテンシを抑えつつスループットを最大化 できます。

SIMD対応を入れたところ、CI上でWASM buildが失敗した。
https://github.com/SH11235/shogi/actions/runs/16256531420/job/45893695578

発覚したタイミングは以下のcommit。

Claude Codeが言うにはWASMでは並列処理が未サポートという事らしい。

  WASMでのSIMD/並列処理の制限

  1. 並列処理(Rayon)

  - WASMは単一スレッドモデルのため、Rayonなどの並列処理ライブラリは使用できません
  - Web Workersを使った並列処理は可能ですが、SharedArrayBufferやAtomicsが必要で、セキュリティ制
  限があります

  2. SIMD最適化

  - WASM MVPではSIMD命令はサポートされていません
  - WASM SIMD提案は存在しますが、まだ全ブラウザで有効ではありません
  - 現在のコードのsrc/ai/nnue/simd/wasm32.rsは将来のWASM
  SIMD対応を見据えたものですが、実際には使用されていません

  3. 現在のWASM版の動作

  // src/ai/nnue/mod.rsで条件分岐されているはず
  #[cfg(target_arch = "wasm32")]
  mod wasm32;  // 将来のSIMD対応用

  #[cfg(not(target_arch = "wasm32"))]
  mod native;  // x86_64やARM向けのSIMD実装

  実際にはWASM版では:
  - 基本的なスカラー演算のみ使用
  - SIMD最適化なし
  - 並列処理なし
  - そのため、ネイティブ版と比べて性能は劣る

  4. テーブル制限エラーの原因

  「Only 1 table definition allowed in MVP」エラーは、以下の複合的な要因で発生:
  - lazy_static!マクロが複数のグローバルテーブルを生成
  - 複数のwasm-bindgen外部関数定義
  - 依存クレートも含めた全体でテーブル数が制限を超える

ChatGPT o3に聞くにはそんなことは無い、という話と確かな情報源
https://webassembly.org/news/2025-03-20-wasm-2.0/?utm_source=chatgpt.com

Vector instructions: With a massive 236 new instructions — more than the total number Wasm had before — it now supports 128-bit wide SIMD (single instruction, multiple data) functionality of contemporary CPUs, like Intel’s SSE or ARM’s SVE. This helps speeding up certain classes of compute-intense applications like audio/video codecs, machine learning, and some cryptography.

を出されたので、WASMでもSIMDは実現可能らしい。
Claude Codeの言うように、WASM版ではSIMD最適化を除外する対応自体はやっておいてPull Requestにし、これはmergeせずにCloseして記録に残すこととした。

https://github.com/SH11235/shogi/pull/28

実際はwasm buildに使っているactionがメンテが停止した古いものだったのと、crateのバージョンの問題だった。
localマシンと同じようにwasm-packをインストールしてbuildすればSIMD最適化を含んだWASM buildは問題なく実施出来た。

sh11235sh11235

Rayon crateとSIMDの質問

ChatGPTに聞いた話をメモしておく。

Rustではrayon crateが並列処理を書くときにつかうものとして有名なようです
SIMD実装時にrayon crateは有用ですか? それともrayon無しで書いた方がパフォーマンスやメンテナンス性で有利ですか?

Web検索
https://www.reddit.com/r/rust/comments/1gu8t2j/optimization_adventures_making_a_parallel_rust/
https://users.rust-lang.org/t/parallelism-choosing-between-or-combining-rayon-and-simd/46700
https://nrempel.com/blog/using-simd-for-parallel-processing-in-rust/
https://github.com/rust-lang/packed_simd
https://raphlinus.github.io/rust/simd/2018/10/19/fearless-simd.html

SIMD と Rayon ─ “どちらか”ではなく“うまく棲み分ける”もの

レイヤ 目的 代表 API
SIMD(単一コア内並列) 同じ演算を 128‑512 bit 幅ベクトルで一括処理し、命令数を削減 std::simd, インストリンシック
Rayon(マルチコア並列) データをチャンク分割して複数スレッドへワークスティール実行 par_iter(), join(), ThreadPool

結論だけ先に

  • 高速化ポテンシャルの最大化: 外側 を Rayon でスレッド並列、内側 を SIMD でベクトル化するハイブリッドが最速になることが多い。
  • 維持コスト: Rayon はスレッド生成やワーク分割を自動化してくれるため メンテナンス性が高い。一方 “SIMD だけ/手動スレッドだけ” は制御自由度は高いがコード量とバグリスクが増す。
  • 使わないほうがよいケース: 1 コアで数 µs 未満しか仕事がない 極小 ループ、あるいはメモリ帯域が律速でスレッドを増やしても帯域が飽和している場合。

1. 並列化の階層と相乗効果

┌─────────────┐ (Rayon: マルチスレッド)
│Board batch │ par_chunks(16K)
└─────┬───────┘

┌─────────────┐ (SIMD: 8~16 値同時)
│ NNUE eval │ f32x8 + f32x8 …
└─────────────┘

  • Rayon が扱うのは「ボード 1 万局面 を 16 チャンクに分割し、16 スレッドで並列評価」のような タスク粒度
  • SIMD は「1 局面の評価関数内で 256 bit レジスタ×8 個を回して畳み込み」のような ループ内部のベクトル化
  • それぞれが 別のボトルネック を潰すため、併用すれば乗算的なスピードアップが期待できる。ベンチマークでも “Rayon + SIMD” が単独より高い倍率を示している。

2. パフォーマンス上の判断ポイント

判断軸 Rayon 併用が 向く Rayon 併用が 向かない
処理量/粒度 1 スレッドあたり数百 µs 以上の仕事 数 µs〜十数 µs の超短ループ
メモリ帯域 キャッシュに乗る/演算密度が高い 帯域飽和で CPU が待ち状態
同期頻度 ループ終端のみ(reducecollect など) 頻繁なロック・共有更新がある
ターゲット ネイティブ、サーバ WASM (threads 対応) ブラウザ WASM(threads 未対応)

計測が最重要
Rayon のスレッド切り替え・ワークスティールには 1 タスクあたり ≈15–50 µs の“固定費”がある。Criterion や cargo flamegraph「スレッド 1 つで十分か?」を測るのが最短ルート。


3. 具体的な実装パターン

use rayon::prelude::*;
use std::simd::{f32x8, SimdFloat};

pub fn eval_batch(weights: &[f32], inputs: &mut [f32]) {
    // ① Rayon: バッチをスレッドごとに分割
    inputs.par_chunks_mut(256).for_each(|chunk| {
        // ② SIMD: 各チャンク内部を 8 要素ずつ処理
        for values in chunk.chunks_exact_mut(8) {
            let v = f32x8::from_slice(values);
            let w = f32x8::from_slice(&weights[..8]);
            (v * w).write_to_slice(values);
        }
    });
}
  • par_chunks_mut がチャンク粒度を自動調整(with_min_len, with_max_len でチューニング可)。
  • SIMD で実際の演算を高速化。
  • スレッドプール制御は ThreadPoolBuilder::new().num_threads(n) で CPU 物理コア数に合わせるとオーバーサブスクライブを避けられる。

4. メンテナンス性の観点

観点 Rayon あり Rayon なし(手動スレッド)
コード量 ループ置換 + α で完結 crossbeam 等でスレッド生成・キュー実装
安全性 データ競合を型で防止、&mut 分割で静的保証 ミューテックス・チャネル管理が必要
ポータビリティ cfg(target_has_atomic) 判定で WASM などは自動シングルスレッド 自前で判定が必要
チューニング 分割サイズやプールサイズを API で変更 動的スケジューラを作るなら大変

結論: ほとんどのケースで Rayon の方が保守コストは低い。特殊要件がなければ Rayon + SIMD の二段構え が無難。

5. まとめ

  1. 役割が異なるため併用が基本
    • SIMD = コア内ベクトル化
    • Rayon = マルチコア分散
  2. 使わない判断は “小さすぎる仕事量” と “帯域飽和”
  3. 保守性は Rayon が圧倒的に楽
  4. 計測して閾値を見極める — Criterion, perf, rayon::current_num_threads() などでプロファイル

SIMD 実装時でも Rayon は有用か?

— 答えは Yes。まずは Rayon + SIMD で書き、ボトルネックが見えたらスレッド数・チャンク長・スケジューリングを調整し、オーバーヘッドが厳しい所だけ Rayon を外してみる進め方を推奨。

sh11235sh11235

SIMDとRayonの併用をWASM buildで使うことの質問メモ

SIMDおよびrayonの併用はWASM build時にも有用ですか?

個人的に大きなポイント抜粋

環境依存
Cross-origin isolation を設定できないホスティング(GitHub Pages 既定設定など)は
SharedArrayBuffer が使えず Rayon がシングルスレッド化。ヘッダを返せる CDN/Edge(Cloudflare Pages、Vercel Edge Functions など)は問題なし。

以下は回答ほぼコピペ

Web検索
https://github.com/RReverser/wasm-bindgen-rayon
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
https://www.reddit.com/r/rust/comments/urw1x6/threading_for_wasm_target/
https://forum.dfinity.org/t/wasmtime-with-threads-support-for-easier-porting-of-libraries/30882
https://github.com/bytecodealliance/wasmtime/issues/9466
https://github.com/WebAssembly/spec/blob/main/proposals/simd/SIMD.md
https://emscripten.org/docs/porting/simd.html

TL;DR

  • SIMD は WASM でもほぼどこでも動くsimd128 は Wasm 2.0 正式入り)
  • Rayon 併用は “スレッド対応の WASM ランタイム” なら効果大
    • ブラウザでは SharedArrayBuffer が必要 → cross‑origin isolation を有効化
    • Rust 側は nightly + atomics でビルド、JS 側は wasm-bindgen‑rayon でプール初期化
    • スレッドが使えない環境では Rayon は自動でシングルスレッド落ち(SIMD のみ動作)

1. 現在のサポート状況

機能 ブラウザ Node.js / ランタイム 仕様
SIMD (v128) Chrome 91+ / Firefox 90+ / Safari 15+(デフォルト有効) Node 16.4+、Wasmtime、Wasmer ほか Wasm 2.0 で 128‑bit SIMD(236 命令)追加
Threads (+Atomics) ○ ※COOP/COEP ヘッダ必須(SharedArrayBuffer 解禁) ○(標準で有効) 共有メモリ + Web Workers
Rayon in WASM wasm-bindgen‑rayon が Web Workers + SharedArrayBuffer で実装 ネイティブ std::thread がそのまま動く クレート側でフォールバックあり

2. ビルド手順(Rust → wasm32-unknown-unknown

# ❶ SIMD だけ
RUSTFLAGS="-C target-feature=+simd128" \
  cargo build --target wasm32-unknown-unknown --release

# ❷ SIMD + Rayon (threads)
#    - nightly で std を再ビルド
#    - atomics & bulk‑memory を有効化
RUSTFLAGS="-C target-feature=+simd128,+atomics,+bulk-memory" \
  cargo +nightly build \
    --target wasm32-unknown-unknown \
    -Z build-std=std,panic_abort

JS 側(wasm-bindgen)

import init, { initThreadPool } from "./pkg/your_lib.js";

await init();                                   // 通常初期化
await initThreadPool(navigator.hardwareConcurrency); // Rayon 用プール

✔ SIMD だけなら nightly 不要
✔ Rayon 併用時は nightly + atomics が必須(安定化は議論中)

3. 併用したときの性能と落とし穴

Mandelbrot デモ実測
実装 レンダ時間
シングルスレッド + SIMD 約 270 ms
Rayon 8 threads + SIMD 約 85 ms(≈3×短縮)

ハイブリッド構成(外側 Rayon / 内側 SIMD)が最速になる傾向はネイティブと同じ。

ただし…
オーバーヘッド
Rayon のスレッド生成 + Web Worker 起動は初回数 ms〜数 10 ms。
粒度が数 µs〜数 100 µsしかないループ では元が取れない。

メモリ帯域
JS↔WASM 間コピーが混ざると帯域が頭打ち。
SoA 配列 + バッチ処理が有効。

環境依存
cross‑origin isolation を設定できないホスティング(例: GitHub Pages デフォルト)は
SharedArrayBuffer が使えず Rayon がシングルスレッド化。

4. サーバサイド WASM(WASI / Wasmtime 等)

  • SIMD: wasm32-wasi でも常時有効
  • Threads:
    • Wasmtime 28+ は wasm32-wasi-threads を正式サポート(--wasi threads)
    • Rayon はそのまま動く(std::thread ベース)
    • クロスオリジン制約が無いため設定は簡単

5. 実プロジェクトでの指針

ターゲット 推奨構成
Web ブラウザ 必須: COOP=same-origin, COEP=require-corp ヘッダ
大規模: SIMD + Rayon + wasm-bindgen‑rayon
軽量/レガシー: SIMD だけ/Rayon を cfg(target_feature="atomics") でガード
Node / Deno nightly + atomics ビルドで SIMD + Rayon
Edge / サーバランタイム
(Wasmtime, Wasmer) wasm32-wasi-threads ターゲットでビルドし、Rayon も通常使用

まとめ

  • SIMD は WASM でも標準化済み ― +simd128 フラグで即利用可能
  • Rayon 併用は threads 対応環境で大きな加速
    • ブラウザなら wasm-bindgen‑rayon + cross‑origin isolation
    • 非対応環境でもフォールバックが効く
  • 判断基準は「処理粒度」と「ホストが threads を許すか」
    → まず SIMD + Rayon 版を計測し、必要なら --features=single_thread 等で簡易ビルドを併配布。
sh11235sh11235

最適化最適化最適化...

チェスや将棋の歴史がそこそこあるためか、最適化手法はたくさん知られていて、生成AIは有名な手法は網羅的に知っているので最適化の案をたくさん出してくる。
良かれと思ってそのタスクを進めると、またその先に最適化タスクが発生する。
対戦AI全体像が完成する前に、枝葉末節の最適化タスクをするのは早くモノが欲しい人間視点だと優先順位が間違っている。
この辺は(知らない知識が知れて楽しいのでいいのだが)ある程度の塩梅で切り上げないとMVPさえ完成しない。

何年、何十年の研究や試行錯誤の成果を光速で駆けぬている感じで、全てを吸収しきるのは難しい。
ただ、詳しく知らずとも恩恵を受けられるのはいいことでもある(production codeだと慎重になる具合は変わるけど、趣味開発なら、まあ)。

https://github.com/SH11235/shogi/pull/38
https://github.com/SH11235/shogi/pull/39
https://github.com/SH11235/shogi/pull/41
https://github.com/SH11235/shogi/pull/42
https://github.com/SH11235/shogi/pull/48
https://github.com/SH11235/shogi/pull/49

sh11235sh11235

開発スタイル

2025/07/17時点のやりかた

おそらく作業タスクの難易度があがっている事と、コードベースが大きくなっていることから既存のコードを破壊しないように作業する事が難しくなっており、Planモードで計画を立ててそのままGoではうまくいかなくなってきた。

以前に作ったPhase1~4の大枠の実装計画がある。
今はPhase3の将棋エンジン内部の実装をやっていて、Phase4がWebへの組み込みとなっている。
Phase3のタスクが項目として10数個あるが、これを読んでそのまま走らせるとすぐに破綻する。

実装計画をドキュメント化する

Planモードを使う。これは特に最近始めたものでもない。
この段階で、もし方針で迷うポイントがあれば、選択肢を列挙させてどう迷うのかも述べさせる。
別のセッションでコンテキストをリセットしてから作業させるので、全てファイルに出させる。
このドキュメントは開発用の一時ドキュメントであり、Git管理し続けるのも違うかなと思っていて、これをPull Request本文に使う(この辺はどうしたもんか試行錯誤中)。

プロンプト(Planモード)

〇〇の機能を実装します。
// 何かしらの公式ドキュメントなど参考ドキュメントがあれば読ませる
// リンクを貼って検索させるよりはMarkdownなどファイルに落として読ませる方がいい
// OSSをgit cloneしてきて特定ファイルを読ませるのも手
参考としてpath_to_document を読んで。
実装計画を立ててドキュメントを作って。

実装計画をレビューする

自分が詳しいものは自分でレビューする。
詳しくないものは頑張って知識をいれてレビューする......のは個人開発をサクサクすすめたいモチベーションからするとテンポが悪いので、ChatGPT o3にレビューをさせる。

タスクを細分化する(任意)、実装する

完成した実装計画が大きなスコープである場合は手動でタスクをピックアップする。
ピックアップしたタスクの実装計画が詳細に作れていない場合は再度実装計画を立てて詳細化する。

プロンプト(Planモード)

path_to_document を読んで実装しましょう。

→ToDoリストが作られる
このリストから作業量が大きくなりそうなのが見込めた場合はPlanモードを続けて
プロンプト

タスクnまで実装が終わったら作業を区切りましょう
(ToDoリストで新情報が出た場合)タスクn+1以降について、実装計画ドキュメントを更新して

この後 auto-accept edits で自由に作業をさせる。

途中で実装を諦めてTODOとして作業を止めて、形だけの関数をおくようなこともあるが、タスクの細分化をうまくやっておくと、これの発生確率が落とせる。
基本的にはContextの /compact が発生しない範囲でタスクが完了するのが望ましい。

作業をcommitする

必要なドキュメントはcommitするが、実装計画は恒久的にメンテナンス・参照したいものでは無いのでcommitに入れない(長期のタスクの場合は一旦git管理することもある)。
この時点でformatter, linterが通過する状態にする。
CLAUDE.mdでの指示、もしくはHooksの設定で品質チェックを自動で行うようにしておくのもいい(CLAUDE.mdでの指示はときどき無視される)。
タスクの区切りは基本的にtestを書くところまでとし、既存のテストが通過するところまで整える。
大きな破壊的変更を入れたいのでテストが壊れたまま無視して進んで最後に直すこともあるが、この進め方は難航することがある印象(そもそも破壊的変更を入れる行為自体が設計ミスなので難航するのは順当ではある)。
ここまでを繰り返す。

Pull Request作成、レビュー

ここで実装計画を本文に使い、localにある実装計画ドキュメントは削除する。
レビュアーに以下を使う。

  • Claude Code Actions: CIで自動レビューが走る
  • Copilot: 手動で依頼
  • ChatGPT o3: Pull Request差分のファイルをChatGPTのUIにアップロード(10ファイルまでOK)してレビュー依頼

それぞれで間違った指摘や対応がいらない無駄な最適化を提案してくることがあるので、レビュアー同士のコメントを相互にレビューさせる。
大体はo3にレビューコメントのレビューをさせている。
軽微なものはlocal Claude Code内で妥当な指摘であるなら修正しましょう、として直している。
Pull Request内での@claude でClaude Code Actionsを活用して修正作業をさせてもいいが、修正内容の承認ステップ・commit前の目視での差分チェックを踏める、localでの作業の方が好み。
人間でもそうだが、大きなPull Requestを作るとここのレビューコストがとても上がるので、タスクを細分化しておくのがここでも効いてくる。
Claude Code Actionsのレビューはデフォルトのものを日本語で出力させるようにだけして使っているが、2回目以降のレビューコメントがほぼ同じでノイズになりがちなので、Claude Code Actionsのレビューは一回だけでいいと感じている。
もし大きな変更指摘があった場合はGitHub Issueに切り出して今やらない判断をするか、別ブランチ(別Pull Request)で対応する。

Claude Codeが路頭に迷ったとき

ビルドエラー、リントエラー、テストエラーなどがあったとき、局所的な変更だけを続けてハマり続けることがある。
auto-accept editsのままだとトークンとContextを食い続けて何も改善しないので、escなどで作業を止めて、プロンプトで現状の問題が何かを確認する。

問題が整理されているなら、コンテキストをリセットして再度作業させる。
ChatGPT o3に該当ファイルを渡してみてもらう。
など、ハマり方によって試行錯誤する。
人間がちゃんと実装を読むのでもいい。
この時タスクが大きいとやっぱりコストが跳ね上がるので、タスクの細分化は全体に効いてくる。

GitHub Copilot

VSCode内ではあんまり使わなくなった。
UIのA form label must be associated with an input.biomelint/a11y/noLabelWithoutControl みたいなVSCodeで赤線が出る軽微なlint errorとかを Fix Using Copilot でさっと直させるのに使う。
あとはファイル全部を読ますには大掛かりな微調整をCopilot Chatで済ます。

sh11235sh11235

リファクタ

rust-coreにコアロジックを書いており、crate分割はしていなかったが、wasm buildやデスクトップ向けbuildなどで資材を分けたくなることが想定され、コアロジックなのにwasm buildを意識しないといけないのが気になってきた。
そこでcrate分割をした。
テストやscriptとして使うものも分離出来て、crateの責務が明確になって気分がよい。
多分Claude Codeさんもこの方が理解しやすくなったんじゃないかな。
https://github.com/SH11235/shogi/pull/52

まだ実装はできていないが、Web向けWasm buildのインターフェース、USI(Universal Shogi Interface)プロトコル(将棋AIの大会?などで使われる一般的なものらしい)、デスクトップ(Tauri)向けインターフェース、みたいにアダプタを作って対応環境を増やしていける。

2025/07/25
crate分割はやっててよかった。
設計が無い状態で無秩序に進むと破綻する未来がすぐにくるので、設計を綺麗にしておく恩恵というか、綺麗に保ち続ける事に適切にコストを払い続けないと破綻する。
Rustでコア部分を実装しているが、並列処理周りのムズカシイ話がbuildエラーとして先に潰せたり、cargo fmt , `cargo clippyP が標準であるのでフォーマッターやリンターの設定に悩むこともなくてとても快適。
Rustでコア部分を作るという判断はよかった。今の生成AIツール時代なら自分の言語に対するリテラシーの足りない部分は補いやすい(いくらでも自然言語で質問が出来るので)。

sh11235sh11235

将棋の座標系

例えばエクセルのような行・列のindexと将棋でのそれは異なっていて、将棋盤を先手番目線で見たときに右上が1一、左下が9九という座標になっている。
実装上はこれを認識したうえで、プログラムで効率的に管理できる座標系を持つ必要がある。
また、USIという標準的なプロトコルで表す座標系も異なる。
ということでtestを書く(書かせたのを読む、testエラーにClaude Codeが詰まっている原因を考える)ときに、頭が混乱したのでドキュメントにした。
これを作成する前後で、ここまでの実装が先手番・後手番目線が逆で実装されていたことが判明(重大な不具合)して、修正も入った。

https://github.com/SH11235/shogi/blob/afc2ff11283faf1d084af71e7368fafcbd73ca1a/packages/rust-core/docs/coordinate-system.md

sh11235sh11235

並列処理とパフォーマンステストは難しい

上手く設計すれば、最善手を探索するのをシングルスレッドからマルチスレッドにするとコア数に応じたパフォーマンス改善を測れる。
しかし、この並列処理が入ってくると関連テストがどうしてもflaky testになりがちでCIの実行の度に結果が変わったり、localでも安定しなかったりする。
基本的にパフォーマンステストはrelease buildでのみ実行し、debug buildではスキップするというのがよさそうだ。
https://github.com/SH11235/shogi/commit/bb08263f3e6ae753fd4c11080aa9e31c7fdcc15f
のように、Rustだと以下のコンフィグ設定でdebug buildでの実行をスキップできる。

#![cfg(not(debug_assertions))]

時間管理と探索部の残り時間チェックの仕組みが、シングルスレッドの時は機能したがマルチスレッドになると元の設計が破綻していたようで、すぐに無限ループに落ちる(時間が来ても停止しない)不具合に悩まされた。
vibe codingではやり直しコストは手で書くよりは低く見積もれるので、並列探索と時間管理結合部分の実装を丸々捨てて、再設計した。
Claude Code Opusと、ChatGPT o3の両方を使って、最小構成の実装から初めて、並列処理のパフォーマンスは出ないが、並列処理にしても確実に停止シグナルは受け付けるという状態で少しずつ改善を測る。

sh11235sh11235

GUIで使ってもハングしまくる

ここ数週間バグ取りを続けていた。
GUI部分は一旦世の中で使われているものを使わせていただいて、
http://shogigui.siganus.com/
https://sunfish-shogi.github.io/shogihome/
AI同士で対局させようと試みたが、初手~数手でハングしたり、40手くらいでハングしたりと大量のバグが明らかになった。
しかし、40手でも自動でさしてくれたというのは喜ばしい。とてもとても。

バイブコーディングの弊害というか、最小構成でGUIと連携して動かすという発想を持たずに良かれと思ってエンジンの強化タスクを進めてきたのが問題だったか。
動かない原因を特定して直してもまた動かなくなるのと、Claude Codeに依頼してすんなりタスクが終わることもなくなってきたので、思い切ったコード整理を始めた。
具体的には数千行のサイズのファイルがあるとClaude Codeが部分的に切り取ってファイルを読もうとする挙動があるため、大きなファイルから目をつけてモジュール分割・リファクタリングをした。
この判断は正しかったと思っていて、リファクタリングの過程で未実装機能が発覚したり、能動的にバグを発見したりすることが出来た。
コードが散らっているとまともに作業が出来ないのは人間だけでなく生成AIでも同様(うまくコンテキストを渡さないと生成AIに望みのタスクを完成させられないので自明ではある)。

ハングしまくる問題のある程度が解消出来てきたところ、ShogiHome(Electron、Node.js製)だと初手からハングするが、ShogiGUI(多分C++製だと勝手に思っている)だと対局がちゃんと進むという課題に当たったのが今。

Claude CodeやCodexなどで調査したり質問したりしたところ、システムコールの動作の違いでハングしたりしなかったりしているのではという結論になった。
低レイヤーの話で、普段の自分に馴染みが無いところにぶつかってしまった。
こういうよく分からない課題にぶつかっていくらでも時間をかけていいというのも、趣味プログラミングのいいところかもしれない。

以下、Claude Codeなどに調査してもらったメモ。

ハングの傾向分析

ハングの傾向分析

  1. 共通パターン

すべてのハング箇所で共通しているのは:

  • サブプロセス/パイプI/O環境での実行
  • engine_core APIの呼び出し
  • 正確に6秒間のハング(秒読みのタイムアウト時間)
  1. 具体的なハング箇所

これまでに特定したハング箇所:

  1. is_subprocess_or_piped() - atty::is() の呼び出し

  2. ポジション確認 - engine.has_position() の呼び出し(コメントアウトで回避)

  3. エンジン可用性チェック後 - おそらく adapter.is_engine_available() の後の処理

  4. 根本原因の推測

コメントにも記載されている通り:
// NOTE: has_legal_moves check is implemented but disabled due to MoveGen hang issue
// ...
// but calling it from subprocess context causes a hang. The issue appears to be
// related to complex interaction between subprocess execution and engine_core APIs.

これは以下を示唆しています:

  • サブプロセス環境とengine_core APIの間に相性問題がある
  • 特にI/O操作やシステムコールが関係する処理でハングが発生
  • デバッグビルドでdeadlock_detectorが動いていることも関係している可能性
  1. ハングのメカニズム

おそらく:

  1. サブプロセス環境でstdin/stdout/stderrの状態チェック時にブロック

  2. engine_coreのAPIが内部でI/O操作やロック取得を行う際にデッドロック

  3. 6秒後に外部からstopコマンドが来ると、何らかの形で処理が再開

  4. 次の調査箇所

現在のログでは、ポイント23.2の後でハングしているので:

  • adapter.is_engine_available() の呼び出し
  • またはその後のエンジン可用性チェックのロジック内
  1. 根本的な解決策の方向性
  • サブプロセス環境での実行を前提とした設計に変更
  • I/O操作やシステムコールを含むAPIの使用を避ける
  • タイムアウト付きの操作に置き換える
  • または、サブプロセス検出を完全に無効化して、常にセーフモードで動作させる

現在の回避策はこれらの問題箇所を一つずつスキップしていますが、根本的にはサブプロセス環境でのengine_core
APIの使用方法を見直す必要があるようです。

GUIアプリによるサブプロセスの違い

  1. パイプの作成方法
  • ShogiGUI: 標準的な3つのパイプ(stdin/stdout/stderr)を作成
  • ShogiHome: 特殊なパイプ設定や、stderrを別扱いしている可能性
  1. プロセス起動方法の違い

ShogiGUI の可能性

  • CreateProcess with STARTF_USESTDHANDLES
  • 全てのハンドルが適切に設定されている

ShogiHome の可能性

  • CreateProcess with 特殊なフラグ
  • stderrがリダイレクトされていない
  • または、コンソールウィンドウが関連付けられている
  1. atty::is() がハングする理由

atty::is() は以下のようなシステムコールを使用します:

  • Windows: GetConsoleMode() / GetFileType()
  • Unix: isatty()

ShogiHomeの場合:

  • ハンドルが不完全な状態
  • コンソールモードの取得でブロック
  • ファイルタイプの判定で予期しない動作
  1. 検証方法

以下の環境変数を設定して、動作の違いを確認できます:

// 強制的にパイプとして扱う
std::env::set_var("FORCE_PIPED", "1");

// または、atty::is() の呼び出しを完全にスキップ

  1. GUI別の回避策

// GUI検出ロジックの追加案
fn detect_gui_type() -> GuiType {
// プロセス名やコマンドライン引数から推測
// または特定の環境変数をチェック
}

// GUI別の処理
match detect_gui_type() {
GuiType::ShogiHome => {
// 全ての I/O チェックをスキップ
true
}
GuiType::ShogiGUI => {
// 通常の処理
is_piped_stdio()
}
}

  1. 根本的な解決

最も安全な解決策は:

// 常にサブプロセスモードとして動作
const ALWAYS_SUBPROCESS_MODE: bool = true;

// または起動時オプションで制御
if args.force_subprocess_mode {
// 全ての危険な操作をスキップ
}

  1. デバッグ提案

ShogiHomeで以下を試すと原因が特定できるかもしれません:

  1. 環境変数の設定:
    SKIP_ATTY_CHECK=1
    FORCE_SUBPROCESS_MODE=1
  2. 起動オプション:
    エンジン設定で追加オプションを指定できる場合
  3. ラッパースクリプト:
    エンジンをバッチファイル経由で起動し、環境を制御

このように、GUIアプリケーションの実装によってサブプロセスの扱いは大きく異なり、それが今回のような予期し
ないハングを引き起こす原因となっています。

ShogiGUIはC++製、ShogiHomeはElectron(Node.js)製というように、言語・フレームワーク選択がプロセスの扱い
の違いに影響しそうでしょうか?

● はい、まさにその通りです!言語・フレームワークの違いは、プロセスの扱いに大きく影響します。

C++ vs Electron のプロセス管理の違い

  1. C++ (ShogiGUI) の特徴

// Windows API を直接使用
CreateProcess(
NULL, command,
NULL, NULL, TRUE, // ハンドル継承を明示的に制御
CREATE_NO_WINDOW, // コンソールウィンドウなし
NULL, NULL,
&si, &pi
);

  • 低レベルAPIを直接制御
  • パイプのハンドル管理が明確
  • バッファリングを細かく制御可能
  1. Electron/Node.js (ShogiHome) の特徴

// Node.js の child_process
const engine = spawn('engine.exe', [], {
stdio: ['pipe', 'pipe', 'pipe'], // または 'inherit'
windowsHide: true
});

  • 高レベル抽象化による制御
  • Node.jsのイベントループが介在
  • libuv による I/O 処理
  1. 具体的な違いが生じる理由

パイプの実装

  • C++: Win32 API の CreatePipe() を直接使用
  • Node.js: libuv がクロスプラットフォーム抽象化層を提供

ハンドルの継承

  • C++: SECURITY_ATTRIBUTES で明示的に制御
  • Node.js: 自動的に処理されるが、詳細は隠蔽

コンソールの扱い

// Electron の場合、以下のような違いが生じる可能性
{
stdio: 'pipe', // 標準的なパイプ
detached: true, // 独立したプロセスグループ
shell: false, // シェル経由ではない
windowsVerbatimArguments: true // Windows特有の引数処理
}

  1. atty::is() がハングする原因

Electron/Node.js の場合:

  1. 疑似TTY (Pseudo-TTY):
  • Node.js が疑似TTYを作成する場合がある
  • GetConsoleMode() が予期しない動作
  1. ハンドルの状態:
  • libuvの抽象化により、ハンドルが特殊な状態
  • Windowsのコンソール API が混乱
  1. 非同期I/O:
  • Node.js の非同期I/Oモデルとの相互作用
  • ブロッキング呼び出しが想定外の動作
  1. 検証コード例

// デバッグ用:ハンドルの状態を詳細に調査
#[cfg(windows)]
fn debug_handle_info() {
use winapi::um::processenv::GetStdHandle;
use winapi::um::fileapi::GetFileType;

unsafe {
    let stdin = GetStdHandle(STD_INPUT_HANDLE);
    let file_type = GetFileType(stdin);

    eprintln!("STDIN handle: {:?}, type: {:?}", stdin, file_type);
    // FILE_TYPE_PIPE, FILE_TYPE_CHAR, etc.
}

}

  1. 推奨される回避策

// Electron環境を検出
fn is_electron_environment() -> bool {
// 親プロセス名をチェック
// または特定の環境変数(ELECTRON_RUN_AS_NODE など)
std::env::var("ELECTRON_RUN_AS_NODE").is_ok()
}

// 環境に応じた処理
if is_electron_environment() {
// 全ての tty チェックをスキップ
return true;
}

  1. 根本的な解決策

  2. 環境変数による制御:
    USI_SKIP_TTY_CHECK=1
    USI_FORCE_PIPE_MODE=1

  3. 起動時フラグ:
    engine.exe --no-tty-check

  4. 完全な回避:

  • atty::is() の使用を完全に避ける
  • 常にパイプモードとして動作

このように、C++とElectron/Node.jsの実装の違いが、低レベルのシステムコールの動作に影響を与え、予期しない
ハングを引き起こしている可能性が高いです。

sh11235sh11235

ハングする問題解決

生成AI時代なので捨てて再設計して作り直すというのを1日で出来る。
いい時代だ。
Pull Requestとしては最悪の差分量だが、全捨て・再実装の力技で解決した。
https://github.com/SH11235/shogi/pull/157

MoveGenという指し手を生成するところが問題でハングしていて、Electron(Node.js)環境だとサブプロセスが上手く働かないというのがハングの原因だった。
ここの設計をやり直してシンプルなものとした結果、無事に動くようになった。

この作業はClaude CodeではなくCodexを使って実施した。
最近のClaude Codeはなかなか扱いが難しくなっている感じがあり、すんなりタスクを進めることが少なくなっていたので、別のコーディングエージェントを試したい機運が高まっていたのもある。
GPT5のモデルをつかっての設計書つくり・コーディングは思ったより品質があった。
ただ、Plusプラン(20$/month)だと枠をすぐに使い切ってしまった。
設計書を作るまでで枠を使い切ったので、その設計書をもとにClaude Codeにコーディングをさせると結構さくさく実装が進んでいた。
丁寧な設計書を作ってノイズになるコンテキストを作らないというのがClaude Codeのタスクをうまく進ませるコツであることを再認識した(最近雑にタスクやらせてたかもな...)。
Codex CLIの出来に満足したので、Claude CodeのMAXプラン200$と同じ価格でChatGPTのProプランがあるが、こっちも契約した。
遊ぶお金だと思えばいい・・・?かな。

sh11235sh11235

vibe codingの弊害

1ヶ月くらい無駄な事をしていた。
バグが起きては解消の度に1レイヤー増やし(これが改善だと思い込み)、1レイヤー増やしで何も解決していなかった。
部分的に成功することもあったが、それは根本的な解決ではない短期的な改善でしか無かった。
任せたらそれらしいコードを書いてそれらしいポジティブな報告をしてくるものだからまあいいかで取り入れてしまうとバグの上にバグが重なり(バグでないにしても冗長だったり不要な設計が重なり)、本格的に検証し始めたときにコアな原因が全く見えない状態になる。

何回も楽という名の手抜きをしては後悔をしていたが(これも含めて楽しんではいる)、設計をちゃんとする事と設計通りに作れているかのレビューが大事だ(n回目)。

Codex CLIとClaude Codeはなんだかんだ両方使って作業している。
Claude Codeの調子が悪いとき(時間帯による性能差は明らかにあると思う)や、Context量からしてClaude Codeだとcompactを挟みそうなときはCodex CLIに作業させている。
コードレビューは作業していない方にさせる、などでバランスが取れている感じはする。