]> git.sesse.net Git - stockfish/blobdiff - src/search.cpp
Rewrite early stop logic
[stockfish] / src / search.cpp
index 8257b5bb9f33d308ef668539e75ccb4f89567f12..4304298c73eb4783a7f45e4c20b488f3aa8f567e 100644 (file)
@@ -150,7 +150,7 @@ namespace {
 
   // Easy move margin. An easy move candidate must be at least this much
   // better than the second best move.
-  const Value EasyMoveMargin = Value(0x200);
+  const Value EasyMoveMargin = Value(0x150);
 
 
   /// Namespace variables
@@ -496,16 +496,17 @@ namespace {
     Value bestValues[PLY_MAX_PLUS_2];
     int bestMoveChanges[PLY_MAX_PLUS_2];
     int depth, aspirationDelta;
-    Value value, alpha, beta;
-    Move bestMove, easyMove, skillBest, skillPonder;
+    Value bestValue, alpha, beta;
+    Move bestMove, skillBest, skillPonder;
+    bool bestMoveNeverChanged = true;
 
     // Initialize stuff before a new search
     memset(ss, 0, 4 * sizeof(SearchStack));
     TT.new_search();
     H.clear();
-    *ponderMove = bestMove = easyMove = skillBest = skillPonder = MOVE_NONE;
+    *ponderMove = bestMove = skillBest = skillPonder = MOVE_NONE;
     depth = aspirationDelta = 0;
-    value = alpha = -VALUE_INFINITE, beta = VALUE_INFINITE;
+    bestValue = alpha = -VALUE_INFINITE, beta = VALUE_INFINITE;
     ss->currentMove = MOVE_NULL; // Hack to skip update gains
 
     // Moves to search are verified and copied
@@ -555,7 +556,7 @@ namespace {
             do {
                 // Search starts from ss+1 to allow referencing (ss-1). This is
                 // needed by update gains and ss copy when splitting at Root.
-                value = search<Root>(pos, ss+1, alpha, beta, depth * ONE_PLY);
+                bestValue = search<Root>(pos, ss+1, alpha, beta, depth * ONE_PLY);
 
                 // Bring to front the best move. It is critical that sorting is
                 // done with a stable algorithm because all the values but the first
@@ -569,7 +570,7 @@ namespace {
                 // the fail high/low loop then reorder the PV moves, otherwise
                 // leave the last PV move in its position so to be searched again.
                 // Of course this is needed only in MultiPV search.
-                if (MultiPVIdx && value > alpha && value < beta)
+                if (MultiPVIdx && bestValue > alpha && bestValue < beta)
                     sort<RootMove>(Rml.begin(), Rml.begin() + MultiPVIdx);
 
                 // Write PV back to transposition table in case the relevant entries
@@ -587,7 +588,7 @@ namespace {
                 // if we have a fail high/low and we are deep in the search. UCI
                 // protocol requires to send all the PV lines also if are still
                 // to be searched and so refer to the previous search's score.
-                if ((value > alpha && value < beta) || elapsed_search_time() > 2000)
+                if ((bestValue > alpha && bestValue < beta) || elapsed_search_time() > 2000)
                     for (int i = 0; i < std::min(UCIMultiPV, (int)Rml.size()); i++)
                     {
                         bool updated = (i <= MultiPVIdx);
@@ -608,12 +609,12 @@ namespace {
 
                 // In case of failing high/low increase aspiration window and
                 // research, otherwise exit the fail high/low loop.
-                if (value >= beta)
+                if (bestValue >= beta)
                 {
                     beta = std::min(beta + aspirationDelta, VALUE_INFINITE);
                     aspirationDelta += aspirationDelta / 2;
                 }
-                else if (value <= alpha)
+                else if (bestValue <= alpha)
                 {
                     AspirationFailLow = true;
                     StopOnPonderhit = false;
@@ -624,13 +625,13 @@ namespace {
                 else
                     break;
 
-            } while (abs(value) < VALUE_KNOWN_WIN);
+            } while (abs(bestValue) < VALUE_KNOWN_WIN);
         }
 
         // Collect info about search result
         bestMove = Rml[0].pv[0];
         *ponderMove = Rml[0].pv[1];
-        bestValues[depth] = value;
+        bestValues[depth] = bestValue;
         bestMoveChanges[depth] = Rml.bestMoveChanges;
 
         // Skills: Do we need to pick now the best and the ponder moves ?
@@ -640,28 +641,19 @@ namespace {
         if (Options["Use Search Log"].value<bool>())
         {
             Log log(Options["Search Log Filename"].value<string>());
-            log << pretty_pv(pos, depth, value, elapsed_search_time(), &Rml[0].pv[0]) << endl;
+            log << pretty_pv(pos, depth, bestValue, elapsed_search_time(), &Rml[0].pv[0]) << endl;
         }
 
-        // Init easyMove at first iteration or drop it if differs from the best move
-        if (depth == 1 && (Rml.size() == 1 || Rml[0].score > Rml[1].score + EasyMoveMargin))
-            easyMove = bestMove;
-        else if (bestMove != easyMove)
-            easyMove = MOVE_NONE;
+        // Filter out startup noise when monitoring best move stability
+        if (depth > 2 && bestMoveChanges[depth])
+            bestMoveNeverChanged = false;
 
         // Check for some early stop condition
         if (!StopRequest && Limits.useTimeManagement())
         {
-            // Easy move: Stop search early if one move seems to be much better
-            // than the others or if there is only a single legal move. Also in
-            // the latter case search to some depth anyway to get a proper score.
-            if (   depth >= 7
-                && easyMove == bestMove
-                && (   Rml.size() == 1
-                    ||(   Rml[0].nodes > (pos.nodes_searched() * 85) / 100
-                       && elapsed_search_time() > TimeMgr.available_time() / 16)
-                    ||(   Rml[0].nodes > (pos.nodes_searched() * 98) / 100
-                       && elapsed_search_time() > TimeMgr.available_time() / 32)))
+            // Stop search early if there is only a single legal move. Search to
+            // some depth anyway to get a proper score.
+            if (Rml.size() == 1 && depth >= 7)
                 StopRequest = true;
 
             // Take in account some extra time if the best move has changed
@@ -673,6 +665,23 @@ namespace {
             if (elapsed_search_time() > (TimeMgr.available_time() * 62) / 100)
                 StopRequest = true;
 
+            // Stop search early if one move seems to be much better than others
+            if (   depth >= 10
+                && !StopRequest
+                && (   bestMoveNeverChanged
+                    || elapsed_search_time() > (TimeMgr.available_time() * 40) / 100))
+            {
+                Value rBeta = bestValue - EasyMoveMargin;
+                (ss+1)->excludedMove = bestMove;
+                (ss+1)->skipNullMove = true;
+                Value v = search<NonPV>(pos, ss+1, rBeta - 1, rBeta, (depth * ONE_PLY) / 2);
+                (ss+1)->skipNullMove = false;
+                (ss+1)->excludedMove = MOVE_NONE;
+
+                if (v < rBeta)
+                    StopRequest = true;
+            }
+
             // If we are allowed to ponder do not stop the search now but keep pondering
             if (StopRequest && Limits.ponder)
             {
@@ -1027,8 +1036,7 @@ split_point_start: // At split points actual search starts from here
                    << " currmovenumber " << moveCount + MultiPVIdx << endl;
       }
 
-      // At Root and at first iteration do a PV search on all the moves to score root moves
-      isPvMove = (PvNode && moveCount <= (RootNode && depth <= ONE_PLY ? MAX_MOVES : 1));
+      isPvMove = (PvNode && moveCount <= 1);
       givesCheck = pos.move_gives_check(move, ci);
       captureOrPromotion = pos.is_capture_or_promotion(move);
 
@@ -2071,20 +2079,8 @@ split_point_start: // At split points actual search starts from here
 
     do pos.undo_move(pv[--ply]); while (ply);
   }
-} // namespace
-
-
-// Little helper used by idle_loop() to check that all the slave threads of a
-// split point have finished searching.
 
-static bool all_slaves_finished(SplitPoint* sp) {
-
-  for (int i = 0; i < Threads.size(); i++)
-      if (sp->is_slave[i])
-          return false;
-
-  return true;
-}
+} // namespace
 
 
 // Thread::idle_loop() is where the thread is parked when it has no work to do.
@@ -2114,7 +2110,7 @@ void Thread::idle_loop(SplitPoint* sp) {
           lock_grab(&sleepLock);
 
           // If we are master and all slaves have finished don't go to sleep
-          if (sp && all_slaves_finished(sp))
+          if (sp && Threads.split_point_finished(sp))
           {
               lock_release(&sleepLock);
               break;
@@ -2166,7 +2162,7 @@ void Thread::idle_loop(SplitPoint* sp) {
 
       // If this thread is the master of a split point and all slaves have
       // finished their work at this split point, return from the idle loop.
-      if (sp && all_slaves_finished(sp))
+      if (sp && Threads.split_point_finished(sp))
       {
           // Because sp->is_slave[] is reset under lock protection,
           // be sure sp->lock has been released before to return.