summaryrefslogtreecommitdiff
path: root/src/command.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/command.c')
-rwxr-xr-xsrc/command.c191
1 files changed, 182 insertions, 9 deletions
diff --git a/src/command.c b/src/command.c
index d528994..4f81b10 100755
--- a/src/command.c
+++ b/src/command.c
@@ -32,6 +32,11 @@ Revision History:
#define MAX_ARGS 32
int g_eModeBeforeAnalyze = -1;
+// Bumped by EvalDnaCommand on every successful `evaldna read`/`load`;
+// EvalCommand compares against this to know its persistent eval-hash
+// cache needs clearing (see EvalCommand for details).
+ULONG g_uDnaGeneration = 0;
+
typedef void (COMMAND_PARSER_FUNCTION)(CHAR *szInput,
ULONG argc,
CHAR *argv[],
@@ -497,19 +502,175 @@ Return value:
**/
{
- SEARCHER_THREAD_CONTEXT *ctx;
+ // rgPawnHash/rgEvalHash are embedded arrays inside
+ // SEARCHER_THREAD_CONTEXT (tens of MB), so allocating fresh +
+ // InitializeSearcherContext's full memset on every call here was
+ // paying that cost per invocation. Keep one persistent context
+ // for the life of the process and use the cheap
+ // ReInitializeSearcherContext (memmove position + zero counters
+ // only) on every call after the first, same pattern the real
+ // search path uses across moves.
+ //
+ // CORRECTNESS: ReInitializeSearcherContext does NOT clear
+ // ctx->rgEvalHash, so a stale score cached under previously-loaded
+ // DNA constants would otherwise be returned forever for any
+ // position hit more than once -- silently ignoring every
+ // subsequent `evaldna read`. g_uDnaGeneration (bumped by
+ // EvalDnaCommand on a successful load) is compared here so a DNA
+ // change forces exactly one full re-init (i.e. one eval-hash
+ // clear per reload, not per position) instead of a stale hit.
+ static SEARCHER_THREAD_CONTEXT *ctx = NULL;
+ static ULONG uLastSeenDnaGeneration = (ULONG)-1;
SCORE i;
- ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT));
- if (NULL != ctx)
+ if (NULL == ctx)
{
+ ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx)
+ {
+ Trace("Out of memory.\n");
+ return;
+ }
InitializeSearcherContext(pos, ctx);
- i = Eval(ctx, -INFINITY, +INFINITY);
- Trace("Static eval: %s\n", ScoreToString(i));
- SystemFreeMemory(ctx);
- } else {
- Trace("Out of memory.\n");
+ uLastSeenDnaGeneration = g_uDnaGeneration;
}
+ else if (uLastSeenDnaGeneration != g_uDnaGeneration)
+ {
+ InitializeSearcherContext(pos, ctx);
+ uLastSeenDnaGeneration = g_uDnaGeneration;
+ }
+ else
+ {
+ ReInitializeSearcherContext(pos, ctx);
+ }
+ i = Eval(ctx, -INFINITY, +INFINITY);
+ Trace("Static eval: %s\n", ScoreToString(i));
+}
+
+
+COMMAND(QSearchCommand)
+/**
+
+Routine description:
+
+ This function implements the 'qsearch' engine command.
+
+ Usage:
+
+ qsearch
+
+ This command takes no arguments and causes the engine to run
+ a quiescence search on the current board position and print
+ the resulting score. Unlike `eval` (a static evaluation with
+ no search at all), this resolves captures/checks/promotions
+ first, the same way the real search does immediately before
+ calling Eval() -- so it's a much better proxy for "is this
+ position actually quiet" than comparing SEE-based capture
+ analysis to a static score.
+
+Parameters:
+
+ The COMMAND macro hides four arguments from the input parser:
+
+ CHAR *szInput : the full line of input
+ ULONG argc : number of argument chunks
+ CHAR *argv[] : array of ptrs to each argument chunk
+ POSITION *pos : a POSITION pointer to operate on
+
+Return value:
+
+ void
+
+**/
+{
+ // Same persistent-context + DNA-generation-invalidation pattern as
+ // EvalCommand (see its comment for the full rationale) -- this
+ // command is meant to be called at corpus scale from the eval
+ // tuner, so paying a fresh ~75MB memset per call would be the
+ // same 1000x throughput hit that command had before it was fixed.
+ static SEARCHER_THREAD_CONTEXT *ctx = NULL;
+ static ULONG uLastSeenDnaGeneration = (ULONG)-1;
+ CUMULATIVE_SEARCH_FLAGS *pf;
+ PLY_INFO *pi;
+ SCORE i;
+
+ if (NULL == ctx)
+ {
+ ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx)
+ {
+ Trace("Out of memory.\n");
+ return;
+ }
+ InitializeSearcherContext(pos, ctx);
+ uLastSeenDnaGeneration = g_uDnaGeneration;
+ }
+ else if (uLastSeenDnaGeneration != g_uDnaGeneration)
+ {
+ InitializeSearcherContext(pos, ctx);
+ uLastSeenDnaGeneration = g_uDnaGeneration;
+ }
+ else
+ {
+ ReInitializeSearcherContext(pos, ctx);
+ }
+
+ // QSearch is normally only ever entered from Search() one ply
+ // below the position it's evaluating (see search.c's "the only
+ // place Qsearch is entered" comment) -- it reads (pi-1)->mv to
+ // learn whether the side to move just got checked into this
+ // position, and asserts ctx->uPly > 0. To call it directly on
+ // the root position, synthesize that one piece of ply-0 state:
+ // a fake "previous move" whose only job is to carry the correct
+ // checking-move flag, so QSearch's in-check handling matches the
+ // position's actual, real InCheck() status instead of silently
+ // taking the no-standpat path incorrectly for a position that IS
+ // in check (or vice versa).
+ ctx->uPly = 1;
+ memset(&ctx->sPlyInfo[0].mv, 0, sizeof(MOVE));
+ if (TRUE == InCheck(&ctx->sPosition, ctx->sPosition.uToMove))
+ {
+ ctx->sPlyInfo[0].mv.uMove |= 0x80000000;
+ }
+
+ pf = &ctx->sSearchFlags;
+ pi = &ctx->sPlyInfo[ctx->uPly];
+ pf->fCouldStandPat[BLACK] = pf->fCouldStandPat[WHITE] = FALSE;
+ pf->uQsearchNodes = pf->uQsearchDepth = 0;
+ pf->uQsearchCheckDepth = QPLIES_OF_NON_CAPTURE_CHECKS;
+ pi->fInQsearch = TRUE;
+
+ // QSearch's recursive move loop checks WE_SHOULD_STOP_SEARCHING,
+ // which reads the process-global g_MoveTimer -- normally set up
+ // by SetMoveTimerForSearch() at the start of every real search.
+ // This command is a standalone call outside that path, so without
+ // this g_MoveTimer is whatever state a prior real search left it
+ // in (e.g. an already-expired hard limit). Save/restore around
+ // the call so a `qsearch` invoked between real searches (e.g.
+ // interactive use after `go`) doesn't leave the timer in a
+ // "think forever" state for whatever search comes next.
+ //
+ // g_uHardExtendLimit is a second process-global with the same
+ // problem, and the one actually responsible for a bogus +MATE
+ // score on every call before this fix: CommonSearchInit's
+ // "uQsearchDepth >= g_uHardExtendLimit" too-deep check (see its
+ // comment) fires immediately when this is left at its zero-
+ // initialized default, since it's normally only ever set once
+ // per real search, inside RootSearch (g_uIterateDepth * 4) --
+ // a path this standalone command never runs through. Set it
+ // generously (MAX_PLY_PER_SEARCH; qsearch's own capture-only move
+ // generation makes runaway recursion a non-issue) for the
+ // duration of the call, same save/restore reasoning as the timer.
+ {
+ MOVE_TIMER SavedMoveTimer = g_MoveTimer;
+ ULONG uSavedHardExtendLimit = g_uHardExtendLimit;
+ SetMoveTimerToThinkForever();
+ g_uHardExtendLimit = MAX_PLY_PER_SEARCH;
+ i = QSearch(ctx, -INFINITY, +INFINITY);
+ g_MoveTimer = SavedMoveTimer;
+ g_uHardExtendLimit = uSavedHardExtendLimit;
+ }
+ Trace("Qsearch score: %s\n", ScoreToString(i));
}
@@ -568,10 +729,16 @@ Return value:
Trace("Error (missing argument)\n");
return;
}
- if (!ReadEvalDNA(argv[2]))
+ if (!ReadEvalDNA(argv[2]))
{
Trace("Error reading dna file.\n");
} else {
+ // EvalCommand's persistent SEARCHER_THREAD_CONTEXT keeps
+ // its embedded eval hash alive across calls for speed; a
+ // DNA reload must invalidate stale cached scores from the
+ // old constants, which this generation bump signals it
+ // to do (see EvalCommand for the actual clear).
+ g_uDnaGeneration++;
Trace("Loaded dna file \"%s\"\n", argv[2]);
p = ExportEvalDNA();
Log("(New) dna: %s\n", p);
@@ -2188,6 +2355,12 @@ COMMAND_PARSER_ENTRY g_ParserTable[] =
TRUE,
FALSE,
"Learn a set of PSQT settings from a PGN file" },
+ { "qsearch",
+ QSearchCommand,
+ FALSE,
+ FALSE,
+ FALSE,
+ "Run a quiescence search on the current position" },
{ "quit",
QuitCommand,
TRUE,