summaryrefslogtreecommitdiff
path: root/src/search.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-09 14:18:52 -0700
committerScott Gasch <[email protected]>2026-10-09 14:18:52 -0700
commitfbdcb9d76dd2444d8bc631331eaeb9435cc247f8 (patch)
tree602ab360462d6f23188ba51f962c4abb2dc6c7e5 /src/search.c
parent73b4afb12abf7cc442831dea42b41b63a2ce07be (diff)
* Grab bag of eval.c changes.
* Don't bother not hashing when moves are pruned. * Prune moves move librerally with LMP. Inexplicably, this is slightly shallower than HEAD (despite pruning MORE). It may be because it is slightly slower. It consistently beats HEAD in match_play.py though (3x now).
Diffstat (limited to 'src/search.c')
-rw-r--r--src/search.c315
1 files changed, 72 insertions, 243 deletions
diff --git a/src/search.c b/src/search.c
index fcf8907..0ac0037 100644
--- a/src/search.c
+++ b/src/search.c
@@ -447,10 +447,43 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
{
POSITION *pos = &ctx->sPosition;
PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly];
+#ifdef DEBUG
+ ASSERT(IS_VALID_SCORE(iAlpha));
+ ASSERT(IS_VALID_SCORE(iBeta));
+ ASSERT(iAlpha < iBeta);
+ ASSERT(ctx->uPly > 0);
+ memcpy(&pi->sPosition, pos, sizeof(POSITION));
+#endif
CUMULATIVE_SEARCH_FLAGS *pf = &ctx->sSearchFlags;
+ ASSERT(IS_VALID_FLAG(pf->fAvoidNullmove));
+ ASSERT(IS_VALID_FLAG(pf->fVerifyNullmove));
+
+ // Jump directly to Qsearch if remaining depth is low enough.
+ // This is the only place Qsearch is entered. Lowered from ONE_PLY to
+ // THREE_QUARTERS_PLY to match the check extension's new flat amount
+ // (searchsup.c's ComputeMoveExtension) -- a lone check (or short run
+ // of them) still buys exactly one extra full-width ply as before, but
+ // a long unbroken chain now pays QUARTER_PLY of real cost per check
+ // instead of extending for free. root.c compensates by trimming the
+ // same QUARTER_PLY off the per-iteration depth budget so this doesn't
+ // just add a blanket 1/4 ply to every search.
+ SCORE iBestScore;
+ if (uDepth < THREE_QUARTERS_PLY)
+ {
+ pf->fCouldStandPat[BLACK] = pf->fCouldStandPat[WHITE] = FALSE;
+ pf->uQsearchNodes = pf->uQsearchDepth = 0;
+ pf->uQsearchCheckDepth = QPLIES_OF_NON_CAPTURE_CHECKS;
+ pi->fInQsearch = TRUE;
+ iBestScore = QSearch(ctx, iAlpha, iBeta);
+ ASSERT(pf->uQsearchNodes < 20000);
+ ASSERT(pf->uQsearchDepth == 0);
+ goto end;
+ }
+ pi->fInQsearch = FALSE;
+ iBestScore = -INFINITY_SCORE;
MOVE mvLast = (pi-1)->mv;
- SCORE iBestScore = -INFINITY_SCORE;
- SCORE iInitialAlpha;
+ ASSERT((mvLast.uMove != 0) || (pf->fAvoidNullmove == TRUE));
+ SCORE iInitialAlpha = iAlpha;
SCORE iEval;
MOVE mv, mvBest, mvHash;
ULONG x = 0;
@@ -507,12 +540,8 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
// LMR-specific rather than part of the shared "unremarkable move"
// definition gate-1 describes.
FLAG fMoveIsPruneReduceEligible = FALSE;
+ ULONG uRawDepthB;
#ifdef PERF_COUNTERS
- // See the node-type counter comment below: guards the surprise
- // counters at end: so a node short-circuited by CommonSearchInit
- // (and therefore never typed for this visit) cannot be scored
- // against a stale eNodeType left by a previous node at this ply.
- FLAG fNoteTypedThisCall = FALSE;
FLAG fThisMoveIsCountermoveMatch = FALSE;
ULONG uCMEvidenceBucket = 0;
ULONG uThisMoveHangBucket = 0;
@@ -536,7 +565,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
ULONG uStage = TRY_HASH_MOVE;
ULONG u;
FLAG fTryEFPHere = FALSE;
- FLAG fAnyMovePruned = FALSE;
FLAG fThisMoveEFPPruned;
FLAG fThisMoveLMPPruned;
SCORE iMoveSee;
@@ -549,40 +577,8 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
UINT64 uNullCalibRealLoopNodeCost = 0;
UINT64 uNullCalibNodesBeforeRealLoop = 0;
#endif
-#ifdef DEBUG
- ASSERT(IS_VALID_SCORE(iAlpha));
- ASSERT(IS_VALID_SCORE(iBeta));
- ASSERT(iAlpha < iBeta);
- ASSERT(ctx->uPly > 0);
- ASSERT((mvLast.uMove != 0) || (pf->fAvoidNullmove == TRUE));
- ASSERT(IS_VALID_FLAG(pf->fAvoidNullmove));
- ASSERT(IS_VALID_FLAG(pf->fVerifyNullmove));
- memcpy(&pi->sPosition, pos, sizeof(POSITION));
-#endif
mvBest.uMove = 0;
- // Jump directly to Qsearch if remaining depth is low enough.
- // This is the only place Qsearch is entered. Lowered from ONE_PLY to
- // THREE_QUARTERS_PLY to match the check extension's new flat amount
- // (searchsup.c's ComputeMoveExtension) -- a lone check (or short run
- // of them) still buys exactly one extra full-width ply as before, but
- // a long unbroken chain now pays QUARTER_PLY of real cost per check
- // instead of extending for free. root.c compensates by trimming the
- // same QUARTER_PLY off the per-iteration depth budget so this doesn't
- // just add a blanket 1/4 ply to every search.
- if (uDepth < THREE_QUARTERS_PLY)
- {
- pf->fCouldStandPat[BLACK] = pf->fCouldStandPat[WHITE] = FALSE;
- pf->uQsearchNodes = pf->uQsearchDepth = 0;
- pf->uQsearchCheckDepth = QPLIES_OF_NON_CAPTURE_CHECKS;
- pi->fInQsearch = TRUE;
- iBestScore = QSearch(ctx, iAlpha, iBeta);
- ASSERT(pf->uQsearchNodes < 20000);
- ASSERT(pf->uQsearchDepth == 0);
- goto end;
- }
- pi->fInQsearch = FALSE;
-
// Common initialization code (which may cause a cutoff or change the
// bounds or decide that we need to stop searching now).
if (TRUE == CommonSearchInit(ctx,
@@ -593,41 +589,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
goto end;
}
-#ifdef PERF_COUNTERS
- // 2026-10-03: denominator for the node-type surprise line, counted
- // HERE rather than inside CommonSearchInit, for two reasons that
- // were both silently corrupting that metric:
- //
- // 1. FULL-WIDTH ONLY. CommonSearchInit is the single funnel both
- // Search() and QSearch() pass through, so counting there swept
- // in qsearch -- ~84% of the tree -- and a qsearch node whose
- // stand-pat already beats beta returns iBestScore >= iBeta
- // immediately. Typed ALL, that lands in "ALL failed high",
- // making the headline number mostly a measure of stand-pat
- // frequency. The question the line's name asks is about
- // full-width node-type prediction, so qsearch is excluded from
- // both numerator (below) and denominator (here).
- // 2. NEVER STALE. Every early-exit path in CommonSearchInit (hash
- // cutoff, repetition draw, ply/depth limit, EGTB hit) returns
- // TRUE *before* pi->eNodeType is assigned, and the caller's
- // `goto end` lands squarely on the surprise counters -- which
- // then scored the node against whatever type the PREVIOUS node
- // at this ply happened to leave behind. That is what produced
- // a "PV failed high/low = 106.2% of PV nodes" reading: more
- // surprises than nodes of that type, which is impossible
- // unless some were attributed to a type the node never had.
- // Reaching this line means CommonSearchInit returned FALSE,
- // i.e. the type really was assigned for THIS visit.
- //
- // NOTE: the NODE_TYPE enum and every behavioral consumer of it
- // (all of which only ever test against NODE_PV) are untouched.
- fNoteTypedThisCall = TRUE;
- ASSERT((ULONG)pi->eNodeType < 3);
- INC(ctx->sCounters.tree.u64NodeTypeCount[(ULONG)pi->eNodeType]);
-#endif
-
DTEnterNode(ctx, uDepth, FALSE, iAlpha, iBeta);
- iInitialAlpha = iAlpha;
pi->eMoveScoringScheme = MOVE_SCORING_NORMAL;
ASSERT((IS_CHECKING_MOVE(mvLast) && (TRUE == pi->fInCheck)) ||
(!IS_CHECKING_MOVE(mvLast) && (FALSE == pi->fInCheck)));
@@ -1025,6 +987,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
fTryEFPHere = TRUE;
}
uStage++;
+ uRawDepthB = DepthBucket(uDepth / ONE_PLY);
ASSERT(x == ctx->sMoveStack.uBegin[ctx->uPly]);
// fall through
@@ -1032,8 +995,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
if (x < ctx->sMoveStack.uEnd[ctx->uPly])
{
ASSERT(x >= ctx->sMoveStack.uBegin[ctx->uPly]);
-
- fIsLeftoverMove = FALSE;
#ifdef PERF_COUNTERS
// Reset every iteration, BEFORE the LMR decision
// block (below) can set it for this move -- without
@@ -1297,18 +1258,23 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
ULONG uFHAttempts = 0;
ULONG uFHPctThousandths = GetMoveFailHighPctThousandths(mv, &uFHAttempts);
+ // Our 4D prediction table, given: late, hang,
+ // remaining depth, and gap to alpha will give us a
+ // pessimistic FH% prediction for the move. Compute
+ // the buckets needed to look it up. Note: depth
+ // bucket is per-node and was already computed (once)
+ // above.
+
// The amount of material this move seems to hang.
SCORE iHangCp = EstimateHangingMaterialCp(ctx, mv, (x - 1),
bbMinorAttacksSnapshot,
bbRookAttacksSnapshot);
-
- // Our 4D prediction table, given: late, hang,
- // remaining depth, and gap to alpha will give us a
- // pessimistic FH% prediction for the move. Compute
- // the buckets needed to look it up.
ULONG uRawDumbB = HangCpBucket(iHangCp);
- ULONG uRawDepthB = DepthBucket(uDepth / ONE_PLY);
+
+ // Gap from iEval to current iAlpha bucket.
ULONG uRawGapB = GapCpBucket((SCORE)(iAlpha - iEval));
+
+ // Late move number bucket.
ULONG uRawLateB = MoveNumBucket(uLegalMoves);
// Given the above signals, the (pessimistic)
@@ -1340,12 +1306,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
uFHAttempts,
uFHPctThousandths,
uPredictedFH);
-#ifdef PERF_COUNTERS
- if (TRUE == fThisMoveLMPPruned)
- {
- INC(ctx->sCounters.tree.u64LMPPruned);
- }
-#endif
// Extended futility pruning (EFP); this is an idea
// proposed by Ernst Heinz (Scalable Search in
@@ -1376,10 +1336,10 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
(uFHPctThousandths <= EFP_FH_PRUNE_THRESHOLD_PCT_THOUSANDTHS);
fThisMoveEFPPruned =
fThisMoveEFPPruned &&
- (uPredictedFH == 0xFFFF || uPredictedFH < 100);
+ (uPredictedFH == 0xFFFF || uPredictedFH < 200);
#if 0
if (fThisMoveEFPPruned &&
- uPredictedFH != 0xFFFF && // JOINT_TABLE_SENTINEL -- fix me
+ uPredictedFH != 0xFFFF &&
uPredictedFH > 100)
{
// Which branch of EstimateHangingMaterialCp
@@ -1414,7 +1374,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
default: szHangSource = "UNKNOWN eMoveScoringScheme"; break;
}
}
-
Trace("Board _AFTER_ the move (%s):\n", MoveToIcs(mv));
DumpPosition(pos);
Trace("iEval = %d\n", iEval);
@@ -1431,12 +1390,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
#endif
}
}
-#ifdef PERF_COUNTERS
- if (TRUE == fThisMoveEFPPruned)
- {
- INC(ctx->sCounters.tree.u64EFPPruned);
- }
-#endif
// If neither LMP nor EFP fired, we are going to search below
// this move. But if the move is very late, we might reduce
@@ -1473,7 +1426,16 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
if ((TRUE == fThisMoveEFPPruned) ||
(TRUE == fThisMoveLMPPruned))
{
- fAnyMovePruned = TRUE;
+#ifdef PERF_COUNTERS
+ if (fThisMoveEFPPruned)
+ {
+ INC(ctx->sCounters.tree.u64EFPPruned);
+ }
+ else if (fThisMoveLMPPruned)
+ {
+ INC(ctx->sCounters.tree.u64LMPPruned);
+ }
+#endif
UnmakeMove(ctx, mv);
ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
continue;
@@ -2008,33 +1970,19 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
if (iAlpha != iInitialAlpha)
{
ASSERT(mvBest.uMove != 0);
- if (!IS_CAPTURE_OR_PROMOTION(mvBest))
- {
- UpdateDynamicMoveOrdering(ctx,
- uDepth,
- mvBest,
- iBestScore,
- 0);
- }
- if (FALSE == fAnyMovePruned)
- {
- StoreExactScore(mvBest,
- pos,
- iBestScore,
- uDepth,
- fThreat,
- ctx->uPly);
- }
- else
- {
- StoreLowerBound(mvBest,
- pos,
- iBestScore,
- uDepth,
- fThreat);
- }
+ UpdateDynamicMoveOrdering(ctx,
+ uDepth,
+ mvBest,
+ iBestScore,
+ 0);
+ StoreExactScore(mvBest,
+ pos,
+ iBestScore,
+ uDepth,
+ fThreat,
+ ctx->uPly);
}
- else if (FALSE == fAnyMovePruned)
+ else
{
// IDEA: "I am very well aware of the fact, that the scores
// you get back outside of the window, are not trustable at
@@ -2066,54 +2014,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
ASSERT(IS_VALID_SCORE(iAlpha));
ASSERT(IS_VALID_SCORE(iBestScore) || WE_SHOULD_STOP_SEARCHING);
ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
-#ifdef PERF_COUNTERS
- // fNodeTypedThisVisit: a node short-circuited by CommonSearchInit
- // (hash cutoff, repetition draw, ply/depth limit, EGTB hit) returns
- // here via `goto end` WITHOUT pi->eNodeType having been assigned for
- // this visit, so scoring it would attribute the outcome to whatever
- // type the previous node at this ply left behind. That is what made
- // this line report "PV failed high/low = 106.2% of PV nodes" --
- // more surprises than nodes of that type.
- if ((!WE_SHOULD_STOP_SEARCHING) && IS_VALID_SCORE(iBestScore) &&
- (TRUE == fNoteTypedThisCall))
- {
- // 2026-10-03: compare against iInitialAlpha, NOT iAlpha.
- // search.c:2048 raises iAlpha IN PLACE as moves improve the
- // score, so by the time we reach end: a node that SUCCESSFULLY
- // raised alpha has iAlpha == iBestScore -- and the old
- // (iBestScore <= iAlpha) test then read TRUE, scoring the
- // ordinary success case as a "fail low". That is why the PV row
- // reported 99.1% surprised: it was measuring "alpha was raised"
- // rather than "the node-type prediction was wrong".
- // iInitialAlpha is the node's alpha on entry (stashed at
- // search.c:626 and already used for exactly this distinction at
- // search.c:2105), which is what a fail-low must be judged
- // against.
- FLAG fFailedLow = (iBestScore <= iInitialAlpha);
- FLAG fFailedHigh = (iBestScore >= iBeta);
- switch (pi->eNodeType)
- {
- case NODE_PV:
- if (fFailedLow || fFailedHigh)
- {
- INC(ctx->sCounters.tree.u64PVNodeSurprised);
- }
- break;
- case NODE_ALL:
- if (fFailedHigh)
- {
- INC(ctx->sCounters.tree.u64AllNodeFailedHigh);
- }
- break;
- case NODE_CUT:
- if (fFailedLow)
- {
- INC(ctx->sCounters.tree.u64CutNodeFailedLow);
- }
- break;
- }
- }
-#endif
#ifdef TEST_NULL
// Commit the calibration record stashed at the diagnostic probe
// site above, now that iBestScore's fate against iBeta (this
@@ -2973,15 +2873,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
#endif
FLAG fIncludeChecks;
FLAG fOrigStandPat = ctx->sSearchFlags.fCouldStandPat[pos->uToMove];
-#ifdef PERF_COUNTERS
- // Entry alpha, for the qsearch node-type accounting at end:. QSearch
- // raises iAlpha IN PLACE (the stand-pat branch and the move loop
- // both do), so a fail-low test against the live iAlpha would score
- // every successful alpha-raise as a fail-low -- the same defect that
- // made Search()'s PV row read 99.1%.
- SCORE iQEntryAlpha = iAlpha;
- FLAG fQNodeTyped = FALSE;
-#endif
static ULONG _WhatToGen[] =
{
GENERATE_CAPTURES_PROMS,
@@ -3006,13 +2897,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
{
goto end;
}
-#ifdef PERF_COUNTERS
- // Reaching here means CommonSearchInit returned FALSE, i.e. it
- // really assigned pi->eNodeType for THIS visit.
- fQNodeTyped = TRUE;
- ASSERT((ULONG)pi->eNodeType < 3);
- INC(ctx->sCounters.tree.u64QNodeTypeCount[(ULONG)pi->eNodeType]);
-#endif
DTEnterNode(ctx, 0, TRUE, iAlpha, iBeta);
// Probe interior node recognizers. Qsearch runs at the deepest,
@@ -3349,61 +3233,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
ASSERT(IS_VALID_SCORE(iBestScore) || WE_SHOULD_STOP_SEARCHING);
DTLeaveNode(ctx, TRUE, iBestScore, pi->mvBest);
-#ifdef PERF_COUNTERS
- // Qsearch half of the node-type accounting -- see chess.h's
- // u64QNodeTypeCount comment for why this is counted separately
- // rather than folded into the full-width counters (it used to be
- // folded in, which made those rates mostly a measure of stand-pat
- // frequency) and why dropping it entirely broke the CUT-FL/ALL-FH
- // identity.
- if ((!WE_SHOULD_STOP_SEARCHING) && IS_VALID_SCORE(iBestScore) &&
- (TRUE == fQNodeTyped))
- {
- FLAG fQLow = (iBestScore <= iQEntryAlpha);
- FLAG fQHigh = (iBestScore >= iBeta);
-
- switch (pi->eNodeType)
- {
- case NODE_PV:
- if (fQLow || fQHigh)
- {
- INC(ctx->sCounters.tree.u64QNodeSurprise[NODE_PV]);
- }
- break;
- case NODE_ALL:
- if (fQHigh)
- {
- INC(ctx->sCounters.tree.u64QNodeSurprise[NODE_ALL]);
- }
- break;
- case NODE_CUT:
- if (fQLow)
- {
- INC(ctx->sCounters.tree.u64QNodeSurprise[NODE_CUT]);
- }
- break;
- }
- }
-#endif
-// 2026-10-03: QSearch's copy of the node-type surprise counters was
-// MOVED to its own slots (above), not deleted. CommonSearchInit types qsearch nodes too (it is the one
-// funnel both Search() and QSearch() pass through), and qsearch is ~84%
-// of the tree -- so these counters were dominated by it. Specifically: a
-// qsearch node whose stand-pat score already beats beta returns
-// iBestScore >= iBeta immediately, and if that node happened to be typed
-// ALL it was scored as "ALL failed high". A stand-pat cutoff is the most
-// ordinary event in qsearch, not a node-type misprediction, which is why
-// the line reported ~47% of all nodes as ALL-failed-high and looked
-// absurd. The question the line's name asks is about FULL-WIDTH
-// node-type prediction, so qsearch is now excluded from both the
-// numerator (here) and the denominator (u64NodeTypeCount, incremented in
-// Search() only).
-//
-// The NODE_TYPE enum itself is deliberately unchanged -- adding a
-// NODE_QNODE value was considered and rejected, because eNodeType feeds
-// eParentType chaining in CommonSearchInit and every behavioral consumer
-// tests against NODE_PV; excluding qsearch from the instrumentation gets
-// the same signal with zero risk to any of that.
// Note: iBestScore can be +INFINITY_SCORE or -INFINITY_SCORE here even in the
// absence of a legitimate mate detected if we disallowed stand