今回はスレッド周りです。
探索を並列化するために並列数だけスレッドの生成が必要です。あと思考時間を監視して、時間になったときに思考を打ち切る判定をするためのスレッドも必要です。
GitHubのC++11用のコードだとスレッド生成はstd::threadを使って実装されています。なお、ここで紹介しているのはC++11用のブランチです。
StockfishのGitHubではMasterに修正が入ると数日以内にC++11用のブランチにも同様の修正が反映されるようなので、特に問題がない限りC++11用のブランチを参照したほうがいいと思います。
私がこのthreadのソースコードを読んでいるとき、「ごちゃごちゃして悪い実装だなぁ」と感じていたのですが、最新版のStockfishではそのごちゃごちゃ感を取り払ったような改善がなされており、かなりシンプルな作りになっています。そんなわけでして最新版のソースコードもご確認ください。
探索ノードのsplit(並列探索時にあるノードを分割して並列探索化すること)の判定条件は将棋の場合は、調整したほうが強くなるとは思いますが、現実的にはこのへんのパラメーターをいじくりまわしてもなかなかいい成果は得られないようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
/* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 Copyright (C) 2004-2008 Tord Romstad (Glaurung author) Copyright (C) 2008-2013 Marco Costalba, Joona Kiiski, Tord Romstad Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Stockfish is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef THREAD_H_INCLUDED #define THREAD_H_INCLUDED #include #include #include #include #include "material.h" #include "movepick.h" #include "pawns.h" #include "position.h" #include "search.h" // 最大思考スレッド数 const int MAX_THREADS = 64; // Because SplitPoint::slavesMask is a uint64_t // 64なのは、SplitPoint::slavesMaskがuint64_tなので64bit分しかないから。 // 1スレッド当たりの参加できるsplit pointの最大数 const int MAX_SPLITPOINTS_PER_THREAD = 8; struct Thread; // splitしたnodeを表現する構造体 // splitしたそれぞれのスレッドから共有して使う。 struct SplitPoint { // Const data after split point has been setup const Position* pos; const Search::Stack* ss; Thread* masterThread; Depth depth; Value beta; int nodeType; Move threatMove; bool cutNode; // Const pointers to shared data // 共有しているデータに対する定数ポインター // splitしたnodeの次の指し手を1手返すために使う。 MovePicker* movePicker; SplitPoint* parentSplitPoint; // Shared data // --- 共有しているデータ // 以下のデータにアクセスするときに用いるlock std::mutex mutex; // splitするときに抑制する(使わない)思考スレッドをbitで表現したもの。 // これがuint64_tなので思考スレッドの最大は64個ということになる。 // このsplitされた部分木において、その探索に参加しているスレッドを表現しており、 // 例えばスレッド番号idxを元にして、1ULL << idx のビットが1であれば、そのスレッドは参加しているとみなせる。 volatile uint64_t slavesMask; volatile int64_t nodes; // 並列探索中のalpha値。alpha値を更新するごとに他のスレッドからはこの値が用いられる。 // readするときはlock不要。writeするときはlockが必要。 volatile Value alpha; volatile Value bestValue; volatile Move bestMove; // この局面で合法(legal()==true)だった指し手の数。これがなければこの局面は詰み。 // MovePickerから1手ずつもらい、それが合法だとわかるごとにインクリメントしていく。 volatile int moveCount; // betaを上回る指し手が見つかり、beta cutoffをするときに探索中の他のスレッドに // そのことを通知するためのフラグ。 volatile bool cutoff; }; /// ThreadBase struct is the base of the hierarchy from where we derive all the /// specialized thread classes. // ThreadBase構造体は、我々が特殊化されたスレッドクラスを派生するクラス階層の底辺となるものである。 // ※ 派生クラスでは、idle_loop()をオーバーライドして用いる。 // スレッドの生成、解体は、thread.cppのnew_thread()とdelete_thread()を用いる。 struct ThreadBase { ThreadBase() : exit(false) {} virtual ~ThreadBase() {} // スレッドが生成されるとこの関数が呼び出される。 // 派生クラスでは、この関数をオーバーライドする。 // ※ タイマースレッドであれば、これがタイマー監視を行うメイン処理にして // ここから、定期的にcheck_time()が呼び出される。 virtual void idle_loop() = 0; // idle_loop()内でsleepしているのを起きるように通知する。 // ※ このクラスの派生クラスのidle_loopでsleepするときは、this->sleepCondition.wait_for()でsleepさせる。 // そうすれば、このnotify_one()で起きることができる。 void notify_one(); // ThreadBase::wait_for()は、引数で指定した変数bがtrueになるまでスレッドをsleepさせる。 // ※ RootPos.this_thread()->wait_for(Signals.stop); のように使う。 void wait_for(volatile const bool& b); // 生成した生のスレッド(std::thread) // この生成は、thread.cpp内にあるヘルパー関数である // new_thread()とdelete_thread()を用いてある。 std::thread nativeThread; // idle_loop()でsleepするときにlockしているmutex // C++11では、condition_variableとセットでmutexが必要なので。 std::mutex mutex; // idle_loop()でsleepしているのを解除するためのシグナル。 // ※ このクラスの派生クラスのidle_loop()では、このsleepするときはシグナルを待つ。 std::condition_variable sleepCondition; // このフラグがtrueになればidle_loop()から抜ける。 // 探索が終了していることが条件。 // そこで停止させたいときは、まずこのフラグをtrueにしたあと、notify_one()を呼び出す。 // ※ また、スレッド停止まで待ちたいならば、nativeThread.join()もそのあとに呼び出す必要があるが、 // thread.cpp内にあるdelete_thread()がそういう実装になっている。 volatile bool exit; }; /// Thread struct keeps together all the thread related stuff like locks, state /// and especially split points. We also use per-thread pawn and material hash /// tables so that once we get a pointer to an entry its life time is unlimited /// and we don't have to care about someone changing the entry under our feet. // Thread構造体はlockやstateや特にsplit pointなどすべてのスレッド関連のものをひとまとめにする。 // スレッドごとのpawnとmaterial hashテーブルも使うことで、ひとたびそのエントリーへのポインターを取得したならば // その生存時間は無期限であり、我々は足元で誰かがエントリーを変更することを心配する必要がない。 struct Thread : public ThreadBase { Thread(); virtual void idle_loop(); bool cutoff_occurred() const; bool available_to(const Thread* master) const; template void split(Position& pos, const Search::Stack* ss, Value alpha, Value beta, Value* bestValue, Move* bestMove, Depth depth, Move threatMove, int moveCount, MovePicker* movePicker, int nodeType, bool cutNode); // 探索のsplitを行った場合、ここに情報を保持している。 SplitPoint splitPoints[MAX_SPLITPOINTS_PER_THREAD]; // materialの計算値をcacheしておくためのtable。8192しか要素のないhash table的。 // Position::material_keyによってアクセスする。 Material::Table materialTable; Endgames endgames; Pawns::Table pawnsTable; Position* activePosition; // 思考スレッドのなかで何番目のスレッドであるか。 // 0~思考スレッド数-1までの間の値がこのクラスのコンストラクタにおいて代入される。 size_t idx; // このスレッドで最大何手深さまで探索したかを示す計測用の変数。 int maxPly; SplitPoint* volatile activeSplitPoint; // splitPointsSize == 0のとき → 現在このスレッドはsplit中の探索に参加していない。 // splitPointsSize != 0のとき → 現在このスレッドはsplit中の探索に参加している。 // splitPoints[splitPointsSize - 1]が現在のsplit point。 volatile int splitPointsSize; // 探索中であるかのフラグ // ※ これがfalseならidleスレッドと思われるので、本当にidleであればsplitなどに参加させることが出来る。 volatile bool searching; }; /// MainThread and TimerThread are derived classes used to characterize the two /// special threads: the main one and the recurring timer. // MainThreadクラスとTimerThreadクラスは、2つの特徴的なスレッドとして用いられる。 // メインの唯一のスレッドと循環的なタイマー(タイマー監視スレッド)である。 struct MainThread : public Thread { // MainThreadのほうは、起動時にthinkingをtrueにしておくことにより、 // start_thinking()で競合するのを回避する。 MainThread() : thinking(true) {} // Avoid a race with start_thinking() virtual void idle_loop(); // このMainThreadが思考中であることを意味するフラグ。これはMainThreadのみが持つ。 volatile bool thinking; }; // タイマー監視スレッド struct TimerThread : public ThreadBase { // TimerThreadは起動時にrunをfalseにしておくことで起動時にはタイマー監視が行われていないことを示す。 // タイマー監視が必要になったらrunをtrueにするべき。 TimerThread() : run(false) {} // このスレッドのメイン処理は、idle_loop()のなかで書かれている。 // このスレッドが不要になったら、delete_thread()を呼び出すこと。これはthread.cppで定義されており、 // ThreadPoolのexit()にて呼び出される。 virtual void idle_loop(); // タイマー監視が不要のときは、これをfalseにする。 // その場合、このスレッドはsleep状態になるが、これはシグナルが来ると起きる。(notify_one()で起きる) bool run; // idle_loop()のなかから、check_time()が呼び出される間隔をミリ秒単位で。 // ※ run==trueのときのみcheck_time()が呼び出される。 // ※ check_time()は、思考の終了時間になっていないかを判定するための処理が書かれている関数。 static const int Resolution = 5; // msec between two check_time() calls }; /// ThreadPool struct handles all the threads related stuff like init, starting, /// parking and, the most important, launching a slave thread at a split point. /// All the access to shared thread data is done through this class. // ThreadPool構造体は、初期化、開始、一時停止、そして重要なものとして、split point(並列化するノード)での // スレイブスレッドの起動のようなスレッド関連のすべてのものを扱う。 // 共有されているthread dataへのすべてのアクセスはこのクラスを通じて行われる。 // ThreadPoolの実体はThread*のvector。 // 生成してある全思考用スレッドをこいつが保持している。 // ただし、Timer監視スレッドもこのクラスが保持している。 // 要するにすべての生成されたスレッドはこのクラスが保持しているので、 // 全スレッドの停止をさせたければ、このクラスのexit()を呼び出せば良い。 struct ThreadPool : public std::vector // コンストラクタもデストラクタもないが、 // 各スレッドは、スレッド生存期間中に初期化されるべき、validであるべきグローバル変数に依存している。 void init(); // No c'tor and d'tor, threads rely on globals that should void exit(); // be initialized and valid during the whole thread lifetime. // メインスレッド // ※ 実装的には、これは、このThreadPoolの1つ目のThreadである。 MainThread* main() { return static_cast // 思考スレッドの生成数に関してuci_option(Options)から情報をもらい、その内容を反映させる。 // ※ スレッド数の変更に伴い、splitする条件の変更などもあるのでそれらをここで行う。 // これは起動時に設定されており、すでに構造体に代入されているのでその内容を反映し、スレッドの生成/消滅を行う。 void read_uci_options(); // 使用できるslave threadがいるのか。いればそれを返す。 Thread* available_slave(const Thread* master) const; // 思考が終わるまでUIスレッドを寝かせるときに使う。 // このクラスのsleepConditionを使って実装してある。 // 通常、UCIの'go'コマンドからならば、次のコマンドライン入力があるまでUIスレッドは寝ているはずだが、 // 1) ベンチマークが思考の終了を待機するとき // 2) start_thinking()内部で、前の思考が終了するのを待つとき // に使う。 void wait_for_think_finished(); // start_thinking()は、新しい探索を開始するためにMainThread::idle_loop()のなかで // sleepしているメインスレッドを起こし、即座に帰る。 // 引数の意味 // const Position&(1つ目の引数) → 探索開始局面。 // Search::LimitsType(2つ目の引数) → 今回の思考時間、持ち時間など。 // searchMoves(3つ目の引数) → 探索する指し手。ないならば、空の配列を渡しておけば、全合法手の意味となる。 // さもなくば、ここで渡した指し手のみを探索する。 // この引数は、UCIプロトコルのgoコマンドの引数として'searchmoves'として指定できる。 // Search::StateStackPtr&(4つ目の引数) → 千日手の検出などをするために探索中の局面を保持しておく構造体 void start_thinking(const Position&, const Search::LimitsType&, const std::vector // Threadがidle_loop()のなかでidle中にsleep(0)で待機するのかどうかのフラグ。 // defaultではfalse。 // trueだとCPU負荷は高くなるがレスポンスは良くなる。 bool sleepWhileIdle; // 最小split深さ。 // UCIプロトコルで"Min Split Depth"として設定された値×ONE_REPLY Depth minimumSplitDepth; // split point当たりの最大スレッド数 // UCIプロトコルで"Max Threads per Split Point"として設定された値 size_t maxThreadsPerSplitPoint; // UIスレッドを寝かせておくときに使うcondition_variable // wait_for_think_finished()でUIスレッドを寝かせるときに内部的に使う。 std::mutex mutex; std::condition_variable sleepCondition; // タイマー監視スレッド // これは特別扱いで、このクラスのvectorのなかには含めない。 TimerThread* timer; }; extern ThreadPool Threads; #endif // #ifndef THREAD_H_INCLUDED |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 |
/* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 Copyright (C) 2004-2008 Tord Romstad (Glaurung author) Copyright (C) 2008-2013 Marco Costalba, Joona Kiiski, Tord Romstad Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Stockfish is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include // ※ std::max()もこれだな。 #include #include "movegen.h" #include "search.h" #include "thread.h" #include "ucioption.h" using namespace Search; // スレッドプール。事前にスレッドを必要数分だけ起動させ、ストックしておく。 ThreadPool Threads; // Global object namespace { // Helpers to launch a thread after creation and joining before delete. Must be // outside Thread c'tor and d'tor because object shall be fully initialized // when virtual idle_loop() is called and when joining. // スレッド生成のあとスレッドを起動させ、スレッドがdeleteされる前にjoinするためのヘルパー群。 // Threadクラスのコンストラクタおよびデストラクタの外側でなければならない。 // なぜならidle_loop()仮想関数が呼び出されときや、joinしているときはオブジェクトは // 完全に初期化されていなければならないので。 // ToDo : いまひとつ意味がわからない。 // TはThreadBaseの派生クラスと仮定している。 // new Tして、std::threadを生成し、idle_loopを呼び出す。このときに引数としてこのnewしたものを渡す。 // また、この関数はnewしたものをリターンする。 // Tとして渡しうるのは、 // TimerThread : タイマースレッド // MainThread : 1つ目の思考スレッド // Thread : 2つ目以降の思考スレッド // である。 template T* th = new T(); th->nativeThread = std::thread(&ThreadBase::idle_loop, th); // Will go to sleep return th; } // スレッドを停止させ、その終了を待つ。 void delete_thread(ThreadBase* th) { // exitフラグをtrueにする。これは探索がすでに終了していなければならない。 th->exit = true; // Search must be already finished // sleepしている可能性があるので起こす。 th->notify_one(); // ネイティブスレッドの終了を待つ。 th->nativeThread.join(); // Wait for thread termination delete th; } } // ThreadBase::notify_one() wakes up the thread when there is some search to do // ThreadBase::notify_one()は、することが何か探索にあるときにidle_loop()のなかで眠っているスレッドを起こすのに使う。 void ThreadBase::notify_one() { // C++11ではmutexとcondition_variableとセットで使うので // notify_one()するときにはこう書くと思っておけば良い。 // ToDo : なぜここだけ"this->"がついているのか? std::unique_lock sleepCondition.notify_one(); } // ThreadBase::wait_for() set the thread to sleep until condition 'b' turns true // ThreadBase::wait_for()は、引数で指定した変数bがtrueになるまでスレッドをsleepさせる。 // ※ RootPos.this_thread()->wait_for(Signals.stop); のように使う。 void ThreadBase::wait_for(volatile const bool& b) { std::unique_lock sleepCondition.wait(lk, [&]{ return b; }); } // Thread c'tor just inits data but does not launch any thread of execution that // instead will be started only upon c'tor returns. // Threadクラスのコンストラクタは単にデータを初期化するだけで、スレッドのいかなる処理も起動させない。 // ToDo : that節以下、意味わからん。"returns"の品詞なに? Thread::Thread() /* : splitPoints() */ { // Value-initialization bug in MSVC // MSVCのバグ回避のために値初期化は↑ではしないのだそうな。 // 探索中フラグはfalse searching = false; // 変数初期化 maxPly = splitPointsSize = 0; activeSplitPoint = nullptr; activePosition = nullptr; // ThreadPool上で何番目の思考スレッドであるか。 // ThreadPoolはglobal objectで、その派生元クラスがvectorなのでsize()を呼び出せば // 何個目であるかが確定する。 // MainThreadはThreadから派生されているので、MainThreadでもこの部分は正しく処理されて、 // MainThreadではidx == 0となる。 idx = Threads.size(); } // TimerThread::idle_loop() is where the timer thread waits msec milliseconds // and then calls check_time(). If msec is 0 thread sleeps until is woken up. // TimerThread::idle_loop() は、タイマースレッドがmsecミリ秒を待ち、check_time()を呼び出す。 // もしmsecが0なら、threadが起こされるまで眠り続ける。 // ※ sleepConditionのシグナルで起こされるまで。 extern void check_time(); void TimerThread::idle_loop() { while (!exit) { // C++11では、condition_variableとセットでmutexが必要なので。 // この時点でmutex.lock()している std::unique_lock // sleepConditionがシグナル状態になるのを待つ。 // タイムアウト条件は、runが0ならINT_MAX(無限に)、そうでなければResolutionだけ。 // runは仕事(ここではTimerThreadとしての役割であるcheck_time()の呼び出し)をすべきかどうかの判定フラグ // これがfalseだとidle_loop()ではシグナルが来るまで眠ったままになる。 // ※ このタイマー監視スレッドを終了させるためには、exit = trueにしたのちに、notify_one()を呼び出す。 // ことであるが、それはdelete_thread()がそういう処理になっている。 if (!exit) sleepCondition.wait_for(lk, std::chrono::milliseconds(run ? Resolution : INT_MAX)); lk.unlock(); // ※ wait_for()の呼び出し後、lock権を再度獲得するので、即座にunlock()してやったほうが、 // notify_one()/notify_all()する他のスレッドがmutexのlock()で待たされなくて済む。 // 思考させている状態であるなら、check_time()を呼び出して、停止すべきかチェック if (run) check_time(); } } // MainThread::idle_loop() is where the main thread is parked waiting to be started // when there is a new search. Main thread will launch all the slave threads. // MainThread::idle_loop()は、新しい探索が必要なとき、メインスレッドが開始させられるのを // 待機する場所である。メインスレッドはすべてのスレイブスレッドを起動する。 void MainThread::idle_loop() { while (true) { std::unique_lock // いまからスレッドを待機するので思考中フラグをfalseにしておく。 // このフラグはstart_thinking()で思考開始時にtrueになる。 // また、start_thinking()には前回の思考が終了するまで待機するコードが入っている。 thinking = false; while (!thinking && !exit) { // もし必要ならばUIスレッドを起動させる Threads.sleepCondition.notify_one(); // Wake up UI thread if needed sleepCondition.wait(lk); } lk.unlock(); // ※ wait_for()の呼び出し後、lock権を再度獲得するので、即座にunlock()してやったほうが、 // notify_one()/notify_all()する他のスレッドがmutexのlock()で待たされなくて済む。 // ここでunlock()しておかないと、Search::think()呼び出し中に、他のスレッドがnotify_one()しようにも // mutex.lock()で待たされてしまう。 // メインスレッドの停止要求が来ていたらプログラムを終了させる。 if (exit) return; // 探索中であるフラグをtrueに searching = true; // 探索開始! Search::think(); assert(searching); // 探索中であるフラグをfalseに searching = false; } } // Thread::cutoff_occurred() checks whether a beta cutoff has occurred in the // current active split point, or in some ancestor of the split point. // Thread::cutoff_occurred()は現在のアクティブなsplit point(分割したノード)か、祖先のsplit pointにおいて // betaカットが生じたかどうかをチェックする。 // 備考) Root nodeでこの関数がtrueになっているとabort扱いで探索が終了。 bool Thread::cutoff_occurred() const { for (SplitPoint* sp = activeSplitPoint; sp; sp = sp->parentSplitPoint) if (sp->cutoff) return true; return false; } // Thread::available_to() checks whether the thread is available to help the // thread 'master' at a split point. An obvious requirement is that thread must // be idle. With more than two threads, this is not sufficient: If the thread is // the master of some split point, it is only available as a slave to the slaves // which are busy searching the split point at the top of slaves split point // stack (the "helpful master concept" in YBWC terminology). // Thread::available_to()は、split pointにおけるスレッドの'master'を手助けするために // 使えるスレッドであるかどうかをチェックする。明らかな要件として、スレッドがidleで // なければならない。しかし、2つ以上のスレッドにおいて、これだけでは十分ではない。 // もし当該スレッドがいくつかのsplit pointのmasterである場合、 // これは当該スレッドのslaveたちのなかで、slaves split point stackの先頭のsplit pointの探索中であるスレッド // のslaveとしてのみ利用可能である。(YBWC用語で"helpful master concept"と呼ばれる。) // cf. // 「激指」におけるゲーム木探索並列化手法 http://www.tkl.iis.u-tokyo.ac.jp/top/modules/newdb/extract/1153/data/geki_para.pdf bool Thread::available_to(const Thread* master) const { // このスレッドが現在探索中であれば利用可能ではないのでfalseを返す。 if (searching) return false; // Make a local copy to be sure doesn't become zero under our feet while // testing next condition and so leading to an out of bound access. // 次の状態テストにおいて我々の足元で(=このあとの処理をしている最中に) // 範囲外のアクセスを導く、splitPointSizeがゼロにならないようにするため論理コピーを作る。 // ※ このあとsplitPoints[splitPointsSize -1]とアクセスするので、 // splitPointsSizeがゼロ以外であるとテストしたあとにsplitPointsSizeが0になってしまうと // 配列の範囲外のアクセスになってしまう。 int size = splitPointsSize; // No split points means that the thread is available as a slave for any // other thread otherwise apply the "helpful master" concept if possible. // split pointsがない(splitPointsSize == 0)とは、他のスレッドがスレーブとして // 利用可能であるスレッドであることを意味し、さもなくば(splitPointsSize != 0なら)、 // 可能ならば、"helpful master"コンセプトを適用する。 // ※ splitPointSize == 0なら、この探索木からsplitされた枝がないことを意味するので // このままどの探索に参加しようと構わない。 // ※ splitPointSize != 0なら、現在探索中のsplitされた探索木の探索の手伝いをする方向で動く。 // (一度splitして、スレッドが割り当てられたからには、そのsplitしたマスターの手伝いに専念するという考え方) // 木として最初のほうの探索でsplitされた部分木ほど優先して探索すべきなので、そのときに割り当てられた // スレーブはその部分木の探索のためのみに使う。 return !size || (splitPoints[size - 1].slavesMask & (1ULL << master->idx)); } // init() is called at startup to create and launch requested threads, that will // go immediately to sleep due to 'sleepWhileIdle' set to true. We cannot use // a c'tor becuase Threads is a static object and we need a fully initialized // engine at this point due to allocation of Endgames in Thread c'tor. // init()は、要求されたスレッド群を生成し起動するために呼び出され、それらは // sleepWhileIdleをtrueにセットするために即座にsleep状態になる。 // 我々はコンストラクタを用いるわけにはならない。なぜなら、Threadsは、static objectであり、 // Threadのコンストラクタの時点で、Endgamesの資源の割り当てのために完全に初期化されたエンジンが // 必要となってしまう。 void ThreadPool::init() { sleepWhileIdle = true; // タイマー監視スレッド // これは特別扱いで、このクラスのvectorのなかには含めない。 timer = new_thread // メインスレッド。1つは必ず生成し、その1番目がメインスレッドであることを保証する。 push_back(new_thread // 思考スレッドの生成数に関してuci_option(Options)から情報をもらい、その内容を反映させる。 // これは起動時に設定されており、すでに構造体に代入されているのでその内容を反映し、スレッドの生成/消滅を行う。 read_uci_options(); } // exit() cleanly terminates the threads before the program exits // exit()は、プログラムの終了前に生成した全スレッドを綺麗に終了させる。 void ThreadPool::exit() { // まずタイマースレッドの終了。 // これを最初にやらないとcheck_time()が呼び出され、そいつが思考スレッドのdataにアクセスしてしまう。 delete_thread(timer); // As first because check_time() accesses threads data // それぞれのThreadに対してdelete_threadを呼び出すだけ。 // delete_thread()はスレッドの停止も待つ。 for (Thread* th : *this) delete_thread(th); } // read_uci_options() updates internal threads parameters from the corresponding // UCI options and creates/destroys threads to match the requested number. Thread // objects are dynamically allocated to avoid creating in advance all possible // threads, with included pawns and material tables, if only few are used. // read_uci_options()は、UCI optionに対応する内部スレッドのパラメーターを更新し、 // 要求された数に応じてスレッドを生成/解体する。使いうるすべてのスレッドを、使用する直前に生成する // ことを避けるために、もし少しであれ使うなら、pawnや駒価値テーブルを含むスレッドのobjectが // 動的に(この時点で)割り当てられる。 // 思考スレッドの生成数に関してuci_option(Options)から情報をもらい、その内容を反映させる。 // これは起動時に設定されており、すでに構造体に代入されているのでその内容を反映し、スレッドの生成/消滅を行う。 void ThreadPool::read_uci_options() { // split point当たりの最大スレッド数 // Optionsの実体は、std::map maxThreadsPerSplitPoint = Options["Max Threads per Split Point"]; // 最小split深さ。 minimumSplitDepth = Options["Min Split Depth"] * ONE_PLY; // option設定により要求されたスレッド数 size_t requested = Options["Threads"]; // スレッドは1個以上でないとおかしい。 assert(requested > 0); // Value 0 has a special meaning: We determine the optimal minimum split depth // automatically. Anyhow the minimumSplitDepth should never be under 4 plies. // (minimumSplitDepthが)0という値は特別な意味を持つ : 我々は最適な最小split深さを自動的に決定する。 // なんにせよminimumSplitDepthが4手以下にすべきではない。 // minimumSplitDepthが0なら、スレッド数が8未満なら4手,8以上なら7手。 // minimumSplitDepthが指定されていれば4手かminimumSplitDepth手の大きいほう。 if (!minimumSplitDepth) minimumSplitDepth = (requested < 8 ? 4 : 7) * ONE_PLY; else minimumSplitDepth = std::max(4 * ONE_PLY, minimumSplitDepth); // リクエストされた個数だけスレッドを生成する。(すでに1個は生成されているが) while (size() < requested) push_back(new_thread // リクエストされた個数が現在生成しているスレッド数を超えているなら解体する。 while (size() > requested) { // 最後のスレッドを停止させ、deleteする。 delete_thread(back()); // 停止させたのでvectorから取り除く。 pop_back(); } } // slave_available() tries to find an idle thread which is available as a slave // for the thread 'master'. // slave_available()は、'master'スレッドがslaveとして利用可能な // idle threadを探すことを試みる。masterスレッドは引数で指定する。 // ※ 見つからなければnullptrが返る。 Thread* ThreadPool::available_slave(const Thread* master) const { // このThreadPoolが保持している全思考スレッドに対して、このmasterにとって // 利用可能であるものを探す。 for (Thread* th : *this) if (th->available_to(master)) return th; return nullptr; } // split() does the actual work of distributing the work at a node between // several available threads. If it does not succeed in splitting the node // (because no idle threads are available), the function immediately returns. // If splitting is possible, a SplitPoint object is initialized with all the // data that must be copied to the helper threads and then helper threads are // told that they have been assigned work. This will cause them to instantly // leave their idle loops and call search(). When all threads have returned from // search() then split() returns. template void Thread::split(Position& pos, const Stack* ss, Value alpha, Value beta, Value* bestValue, Move* bestMove, Depth depth, Move threatMove, int moveCount, MovePicker* movePicker, int nodeType, bool cutNode) { assert(pos.pos_is_ok()); assert(*bestValue <= alpha && alpha < beta && beta <= VALUE_INFINITE); assert(*bestValue > -VALUE_INFINITE); assert(depth >= Threads.minimumSplitDepth); assert(searching); assert(splitPointsSize < MAX_SPLITPOINTS_PER_THREAD); // Pick the next available split point from the split point stack SplitPoint& sp = splitPoints[splitPointsSize]; sp.masterThread = this; sp.parentSplitPoint = activeSplitPoint; sp.slavesMask = 1ULL << idx; sp.depth = depth; sp.bestValue = *bestValue; sp.bestMove = *bestMove; sp.threatMove = threatMove; sp.alpha = alpha; sp.beta = beta; sp.nodeType = nodeType; sp.cutNode = cutNode; sp.movePicker = movePicker; sp.moveCount = moveCount; sp.pos = &pos; sp.nodes = 0; sp.cutoff = false; sp.ss = ss; // Try to allocate available threads and ask them to start searching setting // 'searching' flag. This must be done under lock protection to avoid concurrent // allocation of the same slave by another master. Threads.mutex.lock(); sp.mutex.lock(); ++splitPointsSize; activeSplitPoint = &sp; activePosition = nullptr; size_t slavesCnt = 1; // This thread is always included Thread* slave; while ((slave = Threads.available_slave(this)) != nullptr && ++slavesCnt <= Threads.maxThreadsPerSplitPoint && !Fake) { sp.slavesMask |= 1ULL << slave->idx; slave->activeSplitPoint = &sp; slave->searching = true; // Slave leaves idle_loop() slave->notify_one(); // Could be sleeping } // Everything is set up. The master thread enters the idle loop, from which // it will instantly launch a search, because its 'searching' flag is set. // The thread will return from the idle loop when all slaves have finished // their work at this split point. if (slavesCnt > 1 || Fake) { sp.mutex.unlock(); Threads.mutex.unlock(); Thread::idle_loop(); // Force a call to base class idle_loop() // In helpful master concept a master can help only a sub-tree of its split // point, and because here is all finished is not possible master is booked. assert(!searching); assert(!activePosition); // We have returned from the idle loop, which means that all threads are // finished. Note that setting 'searching' and decreasing splitPointsSize is // done under lock protection to avoid a race with Thread::available_to(). Threads.mutex.lock(); sp.mutex.lock(); } searching = true; --splitPointsSize; activeSplitPoint = sp.parentSplitPoint; activePosition = &pos; pos.set_nodes_searched(pos.nodes_searched() + sp.nodes); *bestMove = sp.bestMove; *bestValue = sp.bestValue; sp.mutex.unlock(); Threads.mutex.unlock(); } // Explicit template instantiations template void Thread::split template void Thread::split< true>(Position&, const Stack*, Value, Value, Value*, Move*, Depth, Move, int, MovePicker*, int, bool); // wait_for_think_finished() waits for main thread to go to sleep then returns // wait_for_think_finished()は、メインスレッドの思考が終了するのを待ち、そしてリターンする。 void ThreadPool::wait_for_think_finished() { std::unique_lock sleepCondition.wait(lk, [&]{ return !main()->thinking; }); } // start_thinking() wakes up the main thread sleeping in MainThread::idle_loop() // so to start a new search, then returns immediately. // start_thinking()は、新しい探索を開始するためにMainThread::idle_loop()のなかで // sleepしているメインスレッドを起こし、即座に帰る。 // ※ searchMovesは探索する指し手。ないならば、空の配列を渡しておけば、全合法手の意味となる。 // さもなくば、ここで渡した指し手のみを探索する。 // (この機能自体はStockfishでは使っていない?) void ThreadPool::start_thinking(const Position& pos, const LimitsType& limits, const std::vector // 前の思考が停止するのを待つ wait_for_think_finished(); // 現在時刻を可能な限り早い段階で保存しておく。 SearchTime = Time::now(); // As early as possible // ponder中ではないので、stopOnPonderhitはfalseに。 // また、初手の探索が終わっていないのでfirstRootMoveもfalseに。 Signals.stopOnPonderhit = Signals.firstRootMove = false; // 停止信号も来ていないのでfalseに。 // rootでfail lowしているかのフラグもfalseに。 Signals.stop = Signals.failedLowAtRoot = false; // 開始局面での全合法手のクリア RootMoves.clear(); // 探索開始局面の設定 RootPos = pos; // 探索条件の設定 Limits = limits; // もし、新しい局面をセットしないのであれば、現在の状態を保持する。 if (states.get()) // If we don't set a new position, preserve current state { SetupStates = std::move(states); // Ownership transfer here // states == SetupStatesだとmoveが行われずに結果としてstates.get() != nullptrのままになるので // このassertは引っかかるのではなかろうか? // ああ、よく見たら、ここのSetupStateはSearch::SetupStateで、 // 引数として渡されたのは、uci.cppのnamespace { SetupState }なのか…。 assert(!states.get()); } // 全合法手に対して、searchMovesに含まれている指し手のみをRootMovesに追加する。 // ただしsearchMovesが空ならばすべての合法手をRootMovesに追加する。 for (const ExtMove& ms : MoveList if (searchMoves.empty() || std::count(searchMoves.begin(), searchMoves.end(), ms.move)) RootMoves.push_back(RootMove(ms.move)); // 思考中フラグ // このフラグには、思考が終了したときidle_loop()のなかでfalseが代入される。 main()->thinking = true; // メインスレッド開始 main()->notify_one(); // Starts main thread } |