From: Marco Costalba Date: Wed, 23 Nov 2011 19:07:29 +0000 (+0100) Subject: Rewrite async I/O X-Git-Url: https://git.sesse.net/?p=stockfish;a=commitdiff_plain;h=ed04c010eb4a569532f322f5030d468380b3ab57 Rewrite async I/O Use the starting thread to wait for GUI input and instead use the other threads to search. The consequence is that now think() is alwasy started on a differnt thread than the caller that returns immediately waiting for input. This reformat greatly simplifies the code and is more in line with the common way to implement this feature. As a side effect now we don't need anymore Makefile tricks with sleep() to allow profile builds. No functional change. Signed-off-by: Marco Costalba --- diff --git a/src/Makefile b/src/Makefile index 1e765faa..86afd58a 100644 --- a/src/Makefile +++ b/src/Makefile @@ -389,7 +389,7 @@ profile-build: $(MAKE) ARCH=$(ARCH) COMP=$(COMP) $(profile_make) @echo "" @echo "Step 2/4. Running benchmark for pgo-build ..." - @sleep 10 | $(PGOBENCH) > /dev/null + @$(PGOBENCH) > /dev/null @echo "" @echo "Step 3/4. Building final executable ..." @touch *.cpp @@ -409,14 +409,14 @@ double-profile-build: $(MAKE) ARCH=x86-64 COMP=$(COMP) $(profile_make) @echo "" @echo "Step 2/6. Running benchmark for pgo-build (popcnt disabled)..." - @sleep 10 | $(PGOBENCH) > /dev/null + @$(PGOBENCH) > /dev/null @echo "" @echo "Step 3/6. Building executable for benchmark (popcnt enabled)..." @touch *.cpp *.h $(MAKE) ARCH=x86-64-modern COMP=$(COMP) $(profile_make) @echo "" @echo "Step 4/6. Running benchmark for pgo-build (popcnt enabled)..." - @sleep 10 | $(PGOBENCH) > /dev/null + @$(PGOBENCH) > /dev/null @echo "" @echo "Step 5/6. Building final executable ..." @touch *.cpp *.h diff --git a/src/benchmark.cpp b/src/benchmark.cpp index 95dd88ac..551c341b 100644 --- a/src/benchmark.cpp +++ b/src/benchmark.cpp @@ -23,6 +23,7 @@ #include "position.h" #include "search.h" +#include "thread.h" #include "ucioption.h" using namespace std; @@ -59,7 +60,6 @@ static const string Defaults[] = { void benchmark(int argc, char* argv[]) { vector fenList; - SearchLimits limits; int64_t totalNodes; int time; @@ -76,11 +76,11 @@ void benchmark(int argc, char* argv[]) { // Search should be limited by nodes, time or depth ? if (valType == "nodes") - limits.maxNodes = atoi(valStr.c_str()); + Limits.maxNodes = atoi(valStr.c_str()); else if (valType == "time") - limits.maxTime = 1000 * atoi(valStr.c_str()); // maxTime is in ms + Limits.maxTime = 1000 * atoi(valStr.c_str()); // maxTime is in ms else - limits.maxDepth = atoi(valStr.c_str()); + Limits.maxDepth = atoi(valStr.c_str()); // Do we need to load positions from a given FEN file? if (fenFile != "default") @@ -107,28 +107,27 @@ void benchmark(int argc, char* argv[]) { // Ok, let's start the benchmark ! totalNodes = 0; time = get_system_time(); + SearchMoves.push_back(MOVE_NONE); for (size_t i = 0; i < fenList.size(); i++) { - Move moves[] = { MOVE_NONE }; Position pos(fenList[i], false, 0); + RootPosition = &pos; cerr << "\nBench position: " << i + 1 << '/' << fenList.size() << endl; if (valType == "perft") { - int64_t cnt = perft(pos, limits.maxDepth * ONE_PLY); + int64_t cnt = perft(pos, Limits.maxDepth * ONE_PLY); - cerr << "\nPerft " << limits.maxDepth + cerr << "\nPerft " << Limits.maxDepth << " nodes counted: " << cnt << endl; totalNodes += cnt; } else { - if (!think(pos, limits, moves)) - break; - + Threads.start_thinking(false); totalNodes += pos.nodes_searched(); } } diff --git a/src/search.cpp b/src/search.cpp index 402433ac..560f64f1 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -43,6 +43,10 @@ using std::cout; using std::endl; using std::string; +SearchLimits Limits; +std::vector SearchMoves; +Position* RootPosition; + namespace { // Set to true to force running with one thread. Used for debugging @@ -162,9 +166,8 @@ namespace { int MultiPV, UCIMultiPV, MultiPVIdx; // Time management variables - volatile bool StopOnPonderhit, FirstRootMove, StopRequest, QuitRequest, AspirationFailLow; + volatile bool StopOnPonderhit, FirstRootMove, StopRequest, AspirationFailLow; TimeManager TimeMgr; - SearchLimits Limits; // Skill level adjustment int SkillLevel; @@ -200,7 +203,6 @@ namespace { string pv_to_uci(const Move pv[], int pvNum, bool chess960); string pretty_pv(Position& pos, int depth, Value score, int time, Move pv[]); string depth_to_uci(Depth depth); - void wait_for_stop_or_ponderhit(); // MovePickerExt template class extends MovePicker and allows to choose at compile // time the proper moves source according to the type of node. In the default case @@ -351,16 +353,17 @@ int64_t perft(Position& pos, Depth depth) { /// variables, and calls id_loop(). It returns false when a "quit" command is /// received during the search. -bool think(Position& pos, const SearchLimits& limits, Move searchMoves[]) { +void think() { static Book book; // Defined static to initialize the PRNG only once + Position& pos = *RootPosition; + // Save "search start" time and reset elapsed time to zero elapsed_search_time(get_system_time()); // Initialize global search-related variables - StopOnPonderhit = StopRequest = QuitRequest = AspirationFailLow = false; - Limits = limits; + StopOnPonderhit = StopRequest = AspirationFailLow = false; // Set output stream mode: normal or chess960. Castling notation is different cout << set960(pos.is_chess960()); @@ -374,11 +377,11 @@ bool think(Position& pos, const SearchLimits& limits, Move searchMoves[]) { Move bookMove = book.probe(pos, Options["Best Book Move"].value()); if (bookMove != MOVE_NONE) { - if (Limits.ponder) - wait_for_stop_or_ponderhit(); + if (!StopRequest && (Limits.ponder || Limits.infinite)) + Threads.wait_for_stop_or_ponderhit(); cout << "bestmove " << bookMove << endl; - return !QuitRequest; + return; } } @@ -432,16 +435,9 @@ bool think(Position& pos, const SearchLimits& limits, Move searchMoves[]) { else Threads.set_timer(100); - // Start async mode to catch UCI commands sent to us while searching, - // like "quit", "stop", etc. - Threads.start_listener(); - // We're ready to start thinking. Call the iterative deepening loop function Move ponderMove = MOVE_NONE; - Move bestMove = id_loop(pos, searchMoves, &ponderMove); - - // From now on any UCI command will be read in-sync with Threads.getline() - Threads.stop_listener(); + Move bestMove = id_loop(pos, &SearchMoves[0], &ponderMove); // Stop timer, no need to check for available time any more Threads.set_timer(0); @@ -469,7 +465,7 @@ bool think(Position& pos, const SearchLimits& limits, Move searchMoves[]) { // we are pondering or in infinite search, we shouldn't print the best move // before we are told to do so. if (!StopRequest && (Limits.ponder || Limits.infinite)) - wait_for_stop_or_ponderhit(); + Threads.wait_for_stop_or_ponderhit(); // Could be MOVE_NONE when searching on a stalemate position cout << "bestmove " << bestMove; @@ -480,8 +476,6 @@ bool think(Position& pos, const SearchLimits& limits, Move searchMoves[]) { cout << " ponder " << ponderMove; cout << endl; - - return !QuitRequest; } @@ -1902,26 +1896,6 @@ split_point_start: // At split points actual search starts from here } - // wait_for_stop_or_ponderhit() is called when the maximum depth is reached - // while the program is pondering. The point is to work around a wrinkle in - // the UCI protocol: When pondering, the engine is not allowed to give a - // "bestmove" before the GUI sends it a "stop" or "ponderhit" command. - // We simply wait here until one of these commands (that raise StopRequest) is - // sent, and return, after which the bestmove and pondermove will be printed. - - void wait_for_stop_or_ponderhit() { - - string cmd; - StopOnPonderhit = true; - - while (!StopRequest) - { - Threads.getline(cmd); - do_uci_async_cmd(cmd); - } - } - - // When playing with strength handicap choose best move among the MultiPV set // using a statistical rule dependent on SkillLevel. Idea by Heinz van Saanen. @@ -2164,15 +2138,34 @@ void Thread::idle_loop(SplitPoint* sp) { } -// do_uci_async_cmd() is called by listener thread when in async mode and 'cmd' -// input line is received from the GUI. +// ThreadsManager::wait_for_stop_or_ponderhit() is called when the maximum depth +// is reached while the program is pondering. The point is to work around a wrinkle +// in the UCI protocol: When pondering, the engine is not allowed to give a +// "bestmove" before the GUI sends it a "stop" or "ponderhit" command. +// We simply wait here until one of these commands (that raise StopRequest) is +// sent, and return, after which the bestmove and pondermove will be printed. + +void ThreadsManager::wait_for_stop_or_ponderhit() { + + StopOnPonderhit = true; + + Thread& main = threads[0]; + + lock_grab(&main.sleepLock); + + while (!StopRequest) + cond_wait(&main.sleepCond, &main.sleepLock); + + lock_release(&main.sleepLock); +} + -void do_uci_async_cmd(const std::string& cmd) { +// uci_async_command() is called when a 'cmd' input line is received from the +// GUI while searching. - if (cmd == "quit") - QuitRequest = StopRequest = true; +void uci_async_command(const std::string& cmd) { - else if (cmd == "stop") + if (cmd == "quit" || cmd == "stop") StopRequest = true; else if (cmd == "ponderhit") diff --git a/src/search.h b/src/search.h index 757aeb00..3262c757 100644 --- a/src/search.h +++ b/src/search.h @@ -20,11 +20,11 @@ #if !defined(SEARCH_H_INCLUDED) #define SEARCH_H_INCLUDED -#include - #include "move.h" #include "types.h" +#include + class Position; struct SplitPoint; @@ -53,21 +53,19 @@ struct SearchStack { struct SearchLimits { - SearchLimits() { memset(this, 0, sizeof(SearchLimits)); } - - SearchLimits(int t, int i, int mtg, int mt, int md, int mn, bool inf, bool pon) - : time(t), increment(i), movesToGo(mtg), maxTime(mt), maxDepth(md), - maxNodes(mn), infinite(inf), ponder(pon) {} - bool useTimeManagement() const { return !(maxTime | maxDepth | maxNodes | infinite); } int time, increment, movesToGo, maxTime, maxDepth, maxNodes, infinite, ponder; }; +extern SearchLimits Limits; +extern std::vector SearchMoves; +extern Position* RootPosition; + extern void init_search(); extern int64_t perft(Position& pos, Depth depth); -extern bool think(Position& pos, const SearchLimits& limits, Move searchMoves[]); -extern void do_uci_async_cmd(const std::string& cmd); +extern void think(); +extern void uci_async_command(const std::string& cmd); extern void do_timer_event(); #endif // !defined(SEARCH_H_INCLUDED) diff --git a/src/thread.cpp b/src/thread.cpp index 4a37d393..57faa6e9 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -38,10 +38,10 @@ namespace { extern "C" { void* start_routine(void* thread) { #endif - if (((Thread*)thread)->threadID == MAX_THREADS) - ((Thread*)thread)->listener_loop(); + if (((Thread*)thread)->threadID == 0) + ((Thread*)thread)->main_loop(); - else if (((Thread*)thread)->threadID == MAX_THREADS + 1) + else if (((Thread*)thread)->threadID == MAX_THREADS) ((Thread*)thread)->timer_loop(); else ((Thread*)thread)->idle_loop(NULL); @@ -124,7 +124,7 @@ void ThreadsManager::set_size(int cnt) { activeThreads = cnt; - for (int i = 0; i < MAX_THREADS; i++) + for (int i = 1; i < MAX_THREADS; i++) // Ignore main thread if (i < activeThreads) { // Dynamically allocate pawn and material hash tables according to the @@ -147,14 +147,14 @@ void ThreadsManager::set_size(int cnt) { void ThreadsManager::init() { - // Initialize sleep condition used to block waiting for GUI input + // Initialize sleep condition used to block waiting for end of searching cond_init(&sleepCond); // Initialize threads lock, used when allocating slaves during splitting lock_init(&threadsLock); // Initialize sleep and split point locks - for (int i = 0; i < MAX_THREADS + 2; i++) + for (int i = 0; i <= MAX_THREADS; i++) { lock_init(&threads[i].sleepLock); cond_init(&threads[i].sleepCond); @@ -164,15 +164,14 @@ void ThreadsManager::init() { } // Initialize main thread's associated data - threads[0].is_searching = true; - threads[0].threadID = 0; - set_size(1); // This makes all the threads but the main to go to sleep + threads[0].pawnTable.init(); + threads[0].materialTable.init(); - // Create and launch all the threads but the main that is already running, - // threads will go immediately to sleep. - for (int i = 1; i < MAX_THREADS + 2; i++) + // Create and launch all the threads, threads will go immediately to sleep + for (int i = 0; i <= MAX_THREADS; i++) { threads[i].is_searching = false; + threads[i].do_sleep = true; threads[i].threadID = i; #if defined(_MSC_VER) @@ -195,21 +194,18 @@ void ThreadsManager::init() { void ThreadsManager::exit() { - for (int i = 0; i < MAX_THREADS + 2; i++) + for (int i = 0; i <= MAX_THREADS; i++) { - if (i != 0) - { - threads[i].do_terminate = true; - threads[i].wake_up(); + threads[i].do_terminate = true; + threads[i].wake_up(); - // Wait for slave termination + // Wait for slave termination #if defined(_MSC_VER) - WaitForSingleObject(threads[i].handle, 0); - CloseHandle(threads[i].handle); + WaitForSingleObject(threads[i].handle, 0); + CloseHandle(threads[i].handle); #else - pthread_join(threads[i].handle, NULL); + pthread_join(threads[i].handle, NULL); #endif - } // Now we can safely destroy locks and wait conditions lock_destroy(&threads[i].sleepLock); @@ -387,7 +383,7 @@ void Thread::timer_loop() { void ThreadsManager::set_timer(int msec) { - Thread& timer = threads[MAX_THREADS + 1]; + Thread& timer = threads[MAX_THREADS]; lock_grab(&timer.sleepLock); timer.maxPly = msec; @@ -396,113 +392,57 @@ void ThreadsManager::set_timer(int msec) { } -// Thread::listener_loop() is where the listener thread, used for I/O, waits for -// input. When is_searching is false then input is read in sync with main thread -// (that blocks), otherwise the listener thread reads any input asynchronously -// and processes the input line calling do_uci_async_cmd(). +// Thread::main_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. -void Thread::listener_loop() { - - std::string cmd; +void Thread::main_loop() { while (true) { lock_grab(&sleepLock); - Threads.inputLine = cmd; - do_sleep = !is_searching; + do_sleep = true; // Always return to sleep after a search + + is_searching = false; - // Here the thread is parked in sync mode after a line has been read - while (do_sleep && !do_terminate) // Catches spurious wake ups + while (do_sleep && !do_terminate) { - cond_signal(&Threads.sleepCond); // Wake up main thread - cond_wait(&sleepCond, &sleepLock); // Sleep here + cond_signal(&Threads.sleepCond); // Wake up UI thread if needed + cond_wait(&sleepCond, &sleepLock); } + is_searching = true; + lock_release(&sleepLock); if (do_terminate) return; - if (!std::getline(std::cin, cmd)) // Block waiting for input - cmd = "quit"; - - lock_grab(&sleepLock); - - // If we are in async mode then process the command now - if (is_searching) - { - // Command "quit" is the last one received by the GUI, so park the - // thread waiting for exiting. Also, after a "stop", for instance on a - // ponder miss, GUI can immediately send the new position to search, - // so return to in-sync mode to avoid discarding good data. - if (cmd == "quit" || cmd == "stop") - is_searching = false; - - do_uci_async_cmd(cmd); - cmd = ""; // Input has been consumed - } - - lock_release(&sleepLock); + think(); // Search entry point } } -// ThreadsManager::getline() is used by main thread to block and wait for input, -// the behaviour mimics std::getline(). - -void ThreadsManager::getline(std::string& cmd) { - - Thread& listener = threads[MAX_THREADS]; +// ThreadsManager::start_thinking() is used by UI thread to wake up the main +// thread parked in main_loop() and starting a new search. If asyncMode is true +// then function returns immediately, otherwise caller is blocked waiting for +// the search to finish. - lock_grab(&listener.sleepLock); +void ThreadsManager::start_thinking(bool asyncMode) { - listener.is_searching = false; // Set sync mode - - // If there is already some input to grab then skip without to wake up the - // listener. This can happen if after we send the "bestmove", the GUI sends - // a command that the listener buffers in inputLine before going to sleep. - if (inputLine.empty()) - { - listener.do_sleep = false; - cond_signal(&listener.sleepCond); // Wake up listener thread - - while (!listener.do_sleep) - cond_wait(&sleepCond, &listener.sleepLock); // Wait for input - } - - cmd = inputLine; - inputLine = ""; // Input has been consumed - - lock_release(&listener.sleepLock); -} - - -// ThreadsManager::start_listener() is called at the beginning of the search to -// swith from sync behaviour (default) to async and so be able to read from UCI -// while other threads are searching. This avoids main thread polling for input. - -void ThreadsManager::start_listener() { - - Thread& listener = threads[MAX_THREADS]; - - lock_grab(&listener.sleepLock); - listener.is_searching = true; - listener.do_sleep = false; - cond_signal(&listener.sleepCond); // Wake up listener thread - lock_release(&listener.sleepLock); -} + Thread& main = threads[0]; + lock_grab(&main.sleepLock); -// ThreadsManager::stop_listener() is called before to send "bestmove" to GUI to -// return to in-sync behaviour. This is needed because while in async mode any -// command is discarded without being processed (except for a very few ones). + // Wait main thread has finished before to launch a new search + while (!main.do_sleep) + cond_wait(&sleepCond, &main.sleepLock); -void ThreadsManager::stop_listener() { + main.do_sleep = false; + cond_signal(&main.sleepCond); // Wake up main thread - Thread& listener = threads[MAX_THREADS]; + if (!asyncMode) + cond_wait(&sleepCond, &main.sleepLock); - lock_grab(&listener.sleepLock); - listener.is_searching = false; - lock_release(&listener.sleepLock); + lock_release(&main.sleepLock); } diff --git a/src/thread.h b/src/thread.h index e6d40094..df42c5aa 100644 --- a/src/thread.h +++ b/src/thread.h @@ -27,6 +27,7 @@ #include "movepick.h" #include "pawns.h" #include "position.h" +#include "search.h" const int MAX_THREADS = 32; const int MAX_ACTIVE_SPLIT_POINTS = 8; @@ -69,7 +70,7 @@ struct Thread { bool cutoff_occurred() const; bool is_available_to(int master) const; void idle_loop(SplitPoint* sp); - void listener_loop(); + void main_loop(); void timer_loop(); SplitPoint splitPoints[MAX_ACTIVE_SPLIT_POINTS]; @@ -116,10 +117,9 @@ public: bool available_slave_exists(int master) const; bool split_point_finished(SplitPoint* sp) const; - void getline(std::string& cmd); - void start_listener(); - void stop_listener(); + void start_thinking(bool asyncMode = true); void set_timer(int msec); + void wait_for_stop_or_ponderhit(); template Value split(Position& pos, SearchStack* ss, Value alpha, Value beta, Value bestValue, @@ -134,7 +134,6 @@ private: int activeThreads; bool useSleepingThreads; WaitCondition sleepCond; - std::string inputLine; }; extern ThreadsManager Threads; diff --git a/src/uci.cpp b/src/uci.cpp index ff804f97..ebb74d43 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -45,7 +45,7 @@ namespace { void set_option(istringstream& up); void set_position(Position& pos, istringstream& up); - bool go(Position& pos, istringstream& up); + void go(Position& pos, istringstream& up); void perft(Position& pos, istringstream& up); } @@ -61,22 +61,22 @@ void uci_loop() { string cmd, token; bool quit = false; - while (!quit) + while (!quit && getline(cin, cmd)) { - Threads.getline(cmd); - istringstream is(cmd); is >> skipws >> token; - if (token == "quit") - quit = true; + quit = (token == "quit"); - else if (token == "stop") - { /* avoid to reply "Unknown command: stop" */ } + if (token == "quit" || token == "stop" || token == "ponderhit") + { + uci_async_command(token); + Threads[0].wake_up(); // In case is waiting for stop or ponderhit + } else if (token == "go") - quit = !go(pos, is); + go(pos, is); else if (token == "ucinewgame") pos.from_fen(StarFEN, false); @@ -190,19 +190,21 @@ namespace { // string, and then calls think(). Returns false if a quit command // is received while thinking, true otherwise. - bool go(Position& pos, istringstream& is) { + void go(Position& pos, istringstream& is) { string token; - SearchLimits limits; - std::vector searchMoves; int time[] = { 0, 0 }, inc[] = { 0, 0 }; + memset(&Limits, 0, sizeof(SearchLimits)); + SearchMoves.clear(); + RootPosition = &pos; + while (is >> token) { if (token == "infinite") - limits.infinite = true; + Limits.infinite = true; else if (token == "ponder") - limits.ponder = true; + Limits.ponder = true; else if (token == "wtime") is >> time[WHITE]; else if (token == "btime") @@ -212,23 +214,23 @@ namespace { else if (token == "binc") is >> inc[BLACK]; else if (token == "movestogo") - is >> limits.movesToGo; + is >> Limits.movesToGo; else if (token == "depth") - is >> limits.maxDepth; + is >> Limits.maxDepth; else if (token == "nodes") - is >> limits.maxNodes; + is >> Limits.maxNodes; else if (token == "movetime") - is >> limits.maxTime; + is >> Limits.maxTime; else if (token == "searchmoves") while (is >> token) - searchMoves.push_back(move_from_uci(pos, token)); + SearchMoves.push_back(move_from_uci(pos, token)); } - searchMoves.push_back(MOVE_NONE); - limits.time = time[pos.side_to_move()]; - limits.increment = inc[pos.side_to_move()]; + SearchMoves.push_back(MOVE_NONE); + Limits.time = time[pos.side_to_move()]; + Limits.increment = inc[pos.side_to_move()]; - return think(pos, limits, &searchMoves[0]); + Threads.start_thinking(); }