summaryrefslogtreecommitdiff
path: root/src/search.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-14 12:36:24 -0700
committerScott Gasch <[email protected]>2026-10-14 12:36:24 -0700
commitbb2d7a9cce6275fcfb038c8a3ab248ba3cf2c827 (patch)
treefd340778cf077ec53df1b144bf191ee8ae8188ea /src/search.c
parent2d7d182d4ab518d9f48c5b8cef0c5f664ed9ba3f (diff)
Extract Search()'s per-move kernel into SearchOneMove (searchsup.c)
UNIFY_SEARCH.md step 2: pull the per-move body out of Search()'s move loop (search.c) -- SEE, MakeMove, extension, LMP/LMR eligibility and decision, PVS search with re-search-on-fail-high, UnmakeMove -- into a standalone function, SearchOneMove (searchsup.c), called from Search() in place of the inline block. HelpSearch() (split.c) is deliberately untouched for now; unifying it onto this same kernel is UNIFY_SEARCH.md's later step. Not extracted (stays in Search(), per UNIFY_SEARCH.md's "what should NOT be extracted"): move acquisition/selection, result publication (iAlpha/mvBest bookkeeping, UpdatePV, hash storage), and the split-launch decision. The split-launch check (StartParallelSearch) runs BEFORE this kernel would be called for that move, unaffected. MOVE_KERNEL_RESULT (chess.h) replaces the old goto/continue control flow a called function can't use: SEARCHED (made, searched, unmade; *piScore valid), PRUNED (LMP fired, never made), ILLEGAL (MakeMove refused mv -- previously silent fall-through with no uLegalMoves bump), ABORTED (WE_SHOULD_STOP_SEARCHING fired after the move was already made/searched/unmade -- *piScore is a real partial result). fFullWindow is an explicit parameter, not inferred from the iBestScore==-INFINITY_SCORE sentinel the inline code used -- that sentinel is only safe because HelpSearch() currently never calls this code at all. Once HelpSearch() also calls SearchOneMove (a later step), its own first move off the shared pool would never be this sentinel's "node's actual first move" in the first place, so making the caller state it explicitly removes a trap that wouldn't assert if violated. FORCEINLINE across the search.c/searchsup.c boundary, no new shared header -- matches this project's own existing convention (WouldGiveCheck, generate.c, non-static FORCEINLINE, called from half a dozen .c files, "one of the most called codepaths... speed of the essence") rather than needing search_inline.h as UNIFY_SEARCH.md originally sketched. Verified actual inlining, not just assumed it: `nm`/objdump on the final linked typhoon binary show no SearchOneMove symbol and no call instruction anywhere -- same as WouldGiveCheck/ComputeMoveExtension/ CanWeReduceOrPruneMove, all of which inline away entirely under this build's -flto, while non-inlined functions (Search, HelpSearch) still show up as expected. Confirms the long parameter list costs nothing at runtime (no call/ret/stack-marshaling survives once genuinely inlined). Diagnostic locals crossing the old inline-block boundary resolved per their actual #ifdef gating (checked, not assumed): fIsLeftoverMove/ fMoveIsPruneReduceEligible and the four uThisMove*Bucket values are read only under #ifdef PERF_COUNTERS post-kernel, so they're OUT params gated the same way; fThisMoveIsCountermoveMatch/uCMEvidenceBucket are caller-computed before the kernel and never mutated by it, so plain IN params under MOVE_ORDERING_EVIDENCE_COLLECTION; fThisMoveJointExtraEligible/uThisMoveJointExtraTier never cross the boundary at all (every consumer is inside the kernel), so they became pure kernel-locals, dropped from Search()'s own declarations entirely. Verification: precommit_check.sh passes. Bit-identical node counts (per UNIFY_SEARCH.md's step-2 gate) confirmed at sd10/--cpus 1 against the pre-extraction baseline (head_reference/pre_unification_sd10_cpus1/) across all 191 positions in the three curated suites, zero differences -- a pure refactor, exactly as required at this stage. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/search.c')
-rw-r--r--src/search.c617
1 files changed, 117 insertions, 500 deletions
diff --git a/src/search.c b/src/search.c
index a517a2d..78c473c 100644
--- a/src/search.c
+++ b/src/search.c
@@ -394,7 +394,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
ULONG x = 0;
SCORE iScore;
INT iPositionalExtend = 0;
- INT iExtend;
ULONG uNextDepth;
ULONG uNullR;
ULONG uLegalMoves = 0;
@@ -416,8 +415,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
ULONG uThisMoveGapBucket = 0;
ULONG uThisMoveNumBucket = 0;
ULONG uThisMoveDepthBucket = 0;
- ULONG uThisMoveJointExtraTier = 0;
- FLAG fThisMoveJointExtraEligible = FALSE;
#endif
HASH_ENTRY sHash;
FLAG fHashHit;
@@ -426,8 +423,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
FLAG fIsDraw;
ULONG uStage = TRY_HASH_MOVE;
ULONG u;
- FLAG fThisMoveLMPPruned;
- SCORE iMoveSee;
+ MOVE_KERNEL_RESULT eMoveKernelResult;
#ifdef TEST_NULL
FLAG fNullCalibEligible = FALSE;
FLAG fNullCalibProbeFH = FALSE;
@@ -899,20 +895,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
if (x < ctx->sMoveStack.uEnd[ctx->uPly])
{
ASSERT(x >= ctx->sMoveStack.uBegin[ctx->uPly]);
-#ifdef PERF_COUNTERS
- // Reset every iteration, BEFORE the LMR decision
- // block (below) can set it for this move -- without
- // this, a move that skips the extra-reduction
- // branch entirely (e.g. GetLMRReduction's own
- // baseline didn't fire) would silently inherit and
- // re-report a PRIOR move's stale tier at the
- // re-search-check site later in this same
- // iteration... except stale across ITERATIONS, not
- // within one -- this reset must happen before this
- // iteration's own LMR decision, not after.
- fThisMoveJointExtraEligible = FALSE;
-#endif
-
// Select the best move to try next.
if (MOVE_SCORING_IID_RESCORED == pi->eMoveScoringScheme)
{
@@ -1061,517 +1043,152 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
}
#endif
- // SEE must be computed on the PRE-move position -- see.c's
- // exchange walk needs the piece still sitting on cFrom.
- // ComputeMoveExtension uses it for two soundness gates: the
- // check extension (Crafty-style: don't extend a checking move
- // that's really just a losing sacrifice) and the recapture
- // extension (don't extend a "recapture" that actually loses
- // material). uDepth <= THREE_PLY mirrors the recapture
- // extension's own depth gate -- cheap to test here since
- // uDepth is already loop-invariant, and it keeps this from
- // computing a SEE on every single non-pawn capture at every
- // depth. One call per move regardless of which gate(s) end up
- // wanting it -- SEE is too hot a routine to pay for twice.
- iMoveSee = 0;
- if (IS_CHECKING_MOVE(mv) ||
- (mv.pCaptured && !IS_PAWN(mv.pCaptured) && (uDepth <= THREE_PLY)))
- {
- iMoveSee = GetMoveSee(ctx,
- mv,
- (uStage == TRY_GENERATED_MOVES) ?
- (x - 1) : (ULONG)-1);
- }
-
- if (TRUE == MakeMove(ctx, mv))
- {
- uLegalMoves++;
- ASSERT((IS_CHECKING_MOVE(mv) && InCheck(pos, pos->uToMove)) ||
- (!IS_CHECKING_MOVE(mv) && !InCheck(pos, pos->uToMove)));
-
- // Compute per-move extension (as opposed to per-position
- // extensions which are represented by iPositionalExtend).
- iExtend = iPositionalExtend;
- ComputeMoveExtension(ctx,
- iAlpha,
- iBeta,
- (x - 1), // Note: x==0 if doing mvHash
- iEval,
- uDepth,
- iMoveSee,
- fIsLeftoverMove,
- &iExtend);
-
- ASSERT((uLegalMoves > 1) ? (x != 0) : TRUE);
-
- // Pruning on alpha... late move pruning / late move reduction.
- fMoveIsPruneReduceEligible = CanWeReduceOrPruneMove(ctx,
- mv,
- x,
- uLegalMoves,
- iExtend,
- iAlpha,
- fThreat,
- fIsLeftoverMove);
- fThisMoveLMPPruned = FALSE;
- if (TRUE == fMoveIsPruneReduceEligible)
- {
- // In here we consider:
- // - LMP (late move pruning), and
- // - LMR (late move reduction).
- //
- // All of these have their own conditions for firing but they
- // are based on dynamic signals about the move. Compute these
- // once, here, at the top instead of N times, below.
-
- // Dynamic move ordering FH attempt and FH/attempts:
- ULONG uFHAttempts = 0;
- ULONG uFHPctThousandths = GetMoveFailHighPctThousandths(mv, &uFHAttempts);
-
- // Our 5D prediction table, given:
- // - the gap from iEval to iAlpha here,
- // - move lateness (number),
- // - board material apparently hung by mv,
- // - remaining depth to search, and
- // - improvement...
- //
- // ...it will give us a predicted FH% for the move.
-
- ULONG uRawGapB = GapCpBucket((SCORE)(iAlpha - iEval));
- ULONG uRawLateB = MoveNumBucket(uLegalMoves);
- SCORE iHangCp = EstimateHangingMaterialCp(ctx, mv, (x - 1),
- bbMinorAttacksSnapshot,
- bbRookAttacksSnapshot);
- ULONG uRawDumbB = HangCpBucket(iHangCp);
-
- // Note: A normal fIsLeftoverMove move has a FH% rate
- // of about 0.4% but this is misleading -- the
- // population is actually bimodal, split between moves
- // near 0% and moves legitimately much higher.
- ULONG uPredictedFH = _JointTablePredictedFHThousandths(uRawDepthB,
- uRawDumbB,
- uRawGapB,
- uRawLateB,
- uRawImproveB);
-
- // Late move pruning; skip searching under this move?
- fThisMoveLMPPruned = ShouldLatePrune(
- ctx,
- uDepth,
- uLegalMoves,
- uFHAttempts,
- uFHPctThousandths,
- uPredictedFH);
-
- // If LMP didn't fire, we are going to search below this
- // move. But if the move is very late, we might reduce
- // the depth of the search a bit. LMR = late move reductions.
- // We never reduce a move such that it drops directly into
- // QSearch though; hence the TWO_PLY requirement below.
- if ((FALSE == fThisMoveLMPPruned) &&
- (uDepth > TWO_PLY))
- {
- ULONG uExtraTier = 0;
- INT iLMR = GetLMRReduction(ctx,
- uDepth,
- uLegalMoves,
- mv,
- iExtend,
- uFHAttempts,
- uFHPctThousandths,
- uPredictedFH,
- &uExtraTier);
- if (iLMR < 0)
- {
- ASSERT(iExtend == 0);
- iExtend = iLMR;
-#ifdef PERF_COUNTERS
- uThisMoveJointExtraTier = uExtraTier;
- fThisMoveJointExtraEligible = TRUE;
- INC(ctx->sCounters.tree.u64JointExtraTries[uExtraTier]);
-#endif
- }
- }
- } // LMP, LMR -- move pruning and reduction
-
- if (TRUE == fThisMoveLMPPruned)
- {
-#ifdef PERF_COUNTERS
- INC(ctx->sCounters.tree.u64LMPPruned);
-#endif
- UnmakeMove(ctx, mv);
- ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
- continue;
- }
-
-#ifdef PERF_COUNTERS
- if (TRUE == fIsLeftoverMove)
- {
- INC(ctx->sCounters.tree.u64LeftoverTries);
-
-#ifdef FH_TABLE_COLLECTION
- if (TRUE == fMoveIsPruneReduceEligible)
- {
- SCORE iHangCp;
-
- INC(ctx->sCounters.tree.u64EligibleLeftoverTries);
- iHangCp = EstimateHangingMaterialCp(
- ctx, mv, x - 1,
- bbMinorAttacksSnapshot,
- bbRookAttacksSnapshot);
- uThisMoveHangBucket = HangCpBucket(iHangCp);
- INC(ctx->sCounters.tree.u64HangEstimateTries[uThisMoveHangBucket]);
-
- // "Down axis": how far below alpha is this
- // node's own static eval? Same eligibility
- // gate as the dumb axis -- iAlpha/iEval are
- // both plain node-local SCOREs (not shared,
- // mutable pos-> fields like bbRookAttacks
- // was), so no snapshot/staleness concern here.
- uThisMoveGapBucket = GapCpBucket(
- (SCORE)(iAlpha - iEval));
- INC(ctx->sCounters.tree.u64GapEstimateTries[uThisMoveGapBucket]);
-
- // "Late axis": this move's own 1-based ordinal
- // position among moves actually tried here.
- uThisMoveNumBucket = MoveNumBucket(uLegalMoves);
- INC(ctx->sCounters.tree.u64MoveNumEstimateTries[uThisMoveNumBucket]);
-
- // "Depth axis": remaining search depth at
- // this node, before this move's own
- // extension/reduction.
- uThisMoveDepthBucket = DepthBucket(uDepth / ONE_PLY);
- INC(ctx->sCounters.tree.u64DepthEstimateTries[uThisMoveDepthBucket]);
-
- // Joint 4D cell, same four bucket indices, in
- // addition to (not instead of) the marginals
- // above -- keeps the marginals live this run as
- // a cheap cross-check that the joint reduces to
- // what we already understand.
- INC(ctx->sCounters.tree.u64JointTries
- [uThisMoveDepthBucket][uThisMoveHangBucket]
- [uThisMoveGapBucket][uThisMoveNumBucket]);
- }
-#endif
- }
-#endif // PERF_COUNTERS
+ // Per-move kernel, extracted to searchsup.c's SearchOneMove --
+ // see UNIFY_SEARCH.md. fFullWindow replaces the old
+ // iBestScore==-INFINITY_SCORE sentinel explicitly (the sentinel
+ // was never safe to reuse once a second caller -- HelpSearch(),
+ // once unified -- might call this with a shared-pool first
+ // move that is NOT this node's first move).
+ eMoveKernelResult = SearchOneMove(ctx,
+ mv,
+ x,
+ (uStage == TRY_GENERATED_MOVES),
+ uDepth,
+ iAlpha,
+ iBeta,
+ iEval,
+ iPositionalExtend,
+ fIsLeftoverMove,
+ fThreat,
+ (iBestScore == -INFINITY_SCORE),
+ uRawDepthB,
+ uRawImproveB,
+ bbMinorAttacksSnapshot,
+ bbRookAttacksSnapshot,
#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
- // Deliberately its own gate, not nested under PERF_COUNTERS
- // above -- see MOI_INC/MOVE_ORDERING_EVIDENCE_COLLECTION
- // comment (chess.h).
- if (TRUE == fThisMoveIsCountermoveMatch)
- {
- MOI_INC(ctx->sCounters.tree.u64CMEvidenceTries[uCMEvidenceBucket]);
- }
+ fThisMoveIsCountermoveMatch,
+ uCMEvidenceBucket,
#endif
- // Record the extension/reduction actually applied to
- // THIS move, unconditionally -- not just when it's
- // negative. Leaving this stale from whatever earlier
- // move at this ply last happened to set it let a child
- // node checking "was my parent's move reduced" via
- // (pi-1)->iExtensionAmount < 0 read a leftover value
- // from a completely different sibling move, not the
- // one actually played.
- pi->iExtensionAmount = iExtend;
- pf->iCumulativeExtend += iExtend;
-
- // Compute the next search depth for this move/subtree.
- uNextDepth = uDepth - ONE_PLY + iExtend;
- if (uNextDepth >= MAX_DEPTH_PER_SEARCH) {
- uNextDepth = 0;
- }
-
- ASSERT(pf->fAvoidNullmove == FALSE);
- if (iBestScore == -INFINITY_SCORE)
- {
- // First move, full a..b window.
- ASSERT(uLegalMoves == 1);
#ifdef TEST_NULL
- // Cost side of the null-move calibration harness'
- // try-vs-skip EV estimate: this is the "R=0"
- // alternative the null probe (see above) is being
- // weighed against -- the real first move's own
- // full-depth subtree cost. uLegalMoves==1 guarantees
- // this fires exactly once per node.
- if (fNullCalibEligible)
- {
- UINT64 u64NodesBeforeFirstMove =
- ctx->sCounters.tree.u64TotalNodeCount;
- iScore = -Search(ctx, -iBeta, -iAlpha, uNextDepth);
- uNullCalibFirstMoveNodeCost =
- ctx->sCounters.tree.u64TotalNodeCount -
- u64NodesBeforeFirstMove;
- }
- else
+ fNullCalibEligible,
+ uNullCalibBucket,
+ &uNullCalibFirstMoveNodeCost,
#endif
- iScore = -Search(ctx, -iBeta, -iAlpha, uNextDepth);
- }
- else
- {
- // Moves 2..N, try a minimal window search
- iScore = -Search(ctx, -iAlpha - 1, -iAlpha, uNextDepth);
- if ((iAlpha < iScore) && (iScore < iBeta))
- {
- iScore = -Search(ctx, -iBeta, -iAlpha, uNextDepth);
- }
- }
-
- // Deeper verification search late move reduction FH.
- if ((iExtend < 0) && (iScore >= iBeta))
- {
+ &uLegalMoves,
+ &iScore
#ifdef PERF_COUNTERS
- // Calibration for the joint-extra-reduction tiers
- // above: did the tier this move was assigned (0 ==
- // GetLMRReduction's baseline only, 1-3 == extra
- // plies the table added) actually predict a
- // re-search here? If low-predicted-FH tiers show an
- // ELEVATED research rate rather than a lower one,
- // that's the distribution-shift failure this
- // project's history keeps producing, caught here
- // (a suite run) instead of in a self-play match.
- if (TRUE == fThisMoveJointExtraEligible)
- {
- INC(ctx->sCounters.tree.u64JointExtraResearch[uThisMoveJointExtraTier]);
- }
+ , &fMoveIsPruneReduceEligible
+ , &uThisMoveHangBucket
+ , &uThisMoveGapBucket
+ , &uThisMoveNumBucket
+ , &uThisMoveDepthBucket
#endif
- // Undo the full reduction, whatever its magnitude...
- uNextDepth -= iExtend;
- pi->iExtensionAmount = 0;
- iScore = -Search(ctx, -iBeta, -iAlpha, uNextDepth);
- }
+ );
+ switch (eMoveKernelResult)
+ {
+ case MOVE_KERNEL_ILLEGAL:
+ case MOVE_KERNEL_PRUNED:
+ continue;
+ case MOVE_KERNEL_ABORTED:
+ iBestScore = iScore;
+ goto end;
+ case MOVE_KERNEL_SEARCHED:
+ default:
+ break;
+ }
+
+ // Check results of search below this move.
+ ASSERT(iBestScore <= iAlpha);
+ ASSERT(iAlpha < iBeta);
+ if (iScore > iBestScore)
+ {
+ iBestScore = iScore;
+ mvBest = mv;
+ pi->mvBest = mv;
-#if defined(PERF_COUNTERS) && defined(JOINT_EXTRA_VERIFY)
- // "Silent miss" shadow verification -- only reachable
- // when the branch above did NOT fire, i.e. this move's
- // extra-reduced score was < beta and so triggered no
- // real re-search. Would removing just the EXTRA plies
- // (restoring the depth HEAD's own baseline -ONE_PLY
- // reduction alone would have used) have found a
- // fail-high we're about to silently miss? Full
- // coverage, every extra-reduced move that reaches here
- // -- see GNUmakefile's JOINT_EXTRA_VERIFY comment for
- // why this is calibration-only, never a normal build.
- else if ((TRUE == fThisMoveJointExtraEligible) &&
- (uThisMoveJointExtraTier > 0))
+ if (iScore > iAlpha)
{
- ULONG uShadowDepth =
- uNextDepth + (uThisMoveJointExtraTier * ONE_PLY);
- SCORE iShadowScore;
-#ifdef NEUTRALIZE_NULL_BAILOUT
- // 2026-09-29: searchsup.c's null-move "history-
- // reduced bailout" branch (Tord Romstad idea) reads
- // (pi-1)->iExtensionAmount -- OUR OWN reduction on
- // mv -- to decide whether to fabricate a "please
- // re-search me" placeholder score instead of a
- // real one. In a real move-loop search that's
- // correct: the fabricated score negates up to
- // exactly iBeta+1 one level up, which search.c's
- // own re-search-on-fail-high machinery correctly
- // interprets and acts on. Our shadow verification
- // is a bespoke one-off Search() call with no such
- // wrapper, so we'd misread that signal as a
- // genuinely-computed fail-high. Temporarily zero
- // OUR OWN pi->iExtensionAmount so this one specific
- // check can't fire for mv -- everything deeper in
- // the shadow subtree still goes through the normal
- // move loop (with its wrapper intact) if the same
- // mechanism legitimately fires there for some
- // other, unrelated reduced move.
- INT iSavedExtensionAmount = pi->iExtensionAmount;
- pi->iExtensionAmount = 0;
-#endif
- iShadowScore =
- -Search(ctx, -iBeta, -iAlpha, uShadowDepth);
-#ifdef NEUTRALIZE_NULL_BAILOUT
- pi->iExtensionAmount = iSavedExtensionAmount;
-#endif
-#ifdef SILENT_MISS_TRACE
- // UpdatePV is only ever called on the iScore >
- // iAlpha branch that does NOT also fail high
- // (search.c's main move loop) -- a beta cutoff
- // jumps straight to "end" without touching the PV
- // array at all, since a fail-high score is only a
- // lower bound, not a real line. Since our whole
- // diagnostic signal IS a fail-high (iShadowScore >=
- // iBeta), WalkPV would always come back empty here
- // -- not useful. What DOES get written on a cutoff
- // is the hash table (StoreLowerBound, just before
- // that same "end" jump), so walk a short hash
- // chain instead, the same side-effect-free idiom
- // FinishPVTailFromHash (util.c) already uses for
- // exactly this purpose.
- CHAR szShadowPV[SMALL_STRING_LEN_CHAR] = "";
+ if (iScore >= iBeta)
{
- MOVE mvChain[8];
- ULONG uChainLen = 0;
- MOVE mvNext;
-
- while ((uChainLen < 8) &&
- (0 != (mvNext = GetPonderMove(pos)).uMove))
+#ifdef PERF_COUNTERS
+ if (TRUE == fIsLeftoverMove)
{
- strncat(szShadowPV, MoveToSan(mvNext, pos),
- ARRAY_LENGTH(szShadowPV) - strlen(szShadowPV) - 1);
- strncat(szShadowPV, " ",
- ARRAY_LENGTH(szShadowPV) - strlen(szShadowPV) - 1);
- mvChain[uChainLen++] = mvNext;
- if (FALSE == MakeMove(ctx, mvNext))
+ INC(ctx->sCounters.tree.u64LeftoverFH);
+ // Gated on fMoveIsPruneReduceEligible now,
+ // matching the tries side -- must
+ // match exactly, or this would reuse a
+ // stale bucket left over from a
+ // different (eligible) move for one
+ // that never got a bucket computed
+ // this iteration.
+ if (TRUE == fMoveIsPruneReduceEligible)
{
- uChainLen--;
- break;
+ INC(ctx->sCounters.tree.u64EligibleLeftoverFH);
+ INC(ctx->sCounters.tree.u64HangEstimateFH[uThisMoveHangBucket]);
+ INC(ctx->sCounters.tree.u64GapEstimateFH[uThisMoveGapBucket]);
+ INC(ctx->sCounters.tree.u64MoveNumEstimateFH[uThisMoveNumBucket]);
+ INC(ctx->sCounters.tree.u64DepthEstimateFH[uThisMoveDepthBucket]);
+ INC(ctx->sCounters.tree.u64JointFH
+ [uThisMoveDepthBucket][uThisMoveHangBucket]
+ [uThisMoveGapBucket][uThisMoveNumBucket]);
}
}
- while (uChainLen > 0)
+#endif // PERF_COUNTERS
+#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
+ if (TRUE == fThisMoveIsCountermoveMatch)
{
- UnmakeMove(ctx, mvChain[--uChainLen]);
+ MOI_INC(ctx->sCounters.tree.u64CMEvidenceFH[uCMEvidenceBucket]);
}
- }
#endif
- // EFP-tier-aligned bucket for the FULLY (base +
- // extra) reduced search's actual remaining depth --
- // see _PostReduceDepthBucketEfpAligned's comment.
- ULONG uPostReduceDepthBucket =
- _PostReduceDepthBucketEfpAligned(uNextDepth);
+ // Update history and killers list and store in
+ // the transposition table.
+ UpdateDynamicMoveOrdering(ctx,
+ uDepth,
+ mv,
+ iScore,
+ x);
+ StoreLowerBound(mv, pos, iScore, uDepth, fThreat);
- INC(ctx->sCounters.tree.u64JointExtraVerifyTries
- [uThisMoveJointExtraTier][uPostReduceDepthBucket]);
- if (iShadowScore >= iBeta)
- {
- INC(ctx->sCounters.tree.u64JointExtraVerifySilentMiss
- [uThisMoveJointExtraTier][uPostReduceDepthBucket]);
- if (iShadowScore == (iBeta + 1))
+ // A fail-high capturing a non-pawn piece is
+ // search-proven evidence that piece was en
+ // prise -- but the victim belongs to the
+ // *other* side, i.e. whoever is to move at
+ // ctx->uPly - 1 (ply parity), not here --
+ // "despite the move you're about to make,
+ // this piece stays in trouble." Skip near
+ // mate: a fail-high there means the whole
+ // subtree is winning regardless of this
+ // particular piece, not that it was
+ // specifically hanging.
+ if (mv.pCaptured && !IS_PAWN(mv.pCaptured) &&
+ (iBeta < +NMATE))
{
- INC(ctx->sCounters.tree.u64JointExtraVerifyNullBailoutSignature
- [uThisMoveJointExtraTier]);
+ ASSERT(ctx->uPly > 0);
+ RecordEnprisePieceAtPly(ctx, ctx->uPly - 1,
+ mv.cTo);
}
-#ifdef SILENT_MISS_TRACE
- // pos currently reflects the position AFTER mv
- // (MakeMove already ran earlier this
- // iteration, UnmakeMove hasn't run yet) --
- // pi->sPosition (the pre-move parent) is
- // DEBUG-only, not available in a release
- // build, so print the POST-move FEN plus the
- // move in coordinate notation (MoveToIcs takes
- // just the move, no pre-move disambiguation
- // context needed) rather than SAN.
- Trace("SILENT_MISS_TRACE tier=%u bucket=%u "
- "uNextDepth=%lu uShadowDepth=%lu "
- "alpha=%d beta=%d "
- "realScore=%d shadowScore=%d "
- "move=%s postMoveFen=\"%s\" "
- "shadowRefutation=\"%s\"\n",
- uThisMoveJointExtraTier,
- uPostReduceDepthBucket,
- uNextDepth, uShadowDepth,
- iAlpha, iBeta,
- iScore, iShadowScore,
- MoveToIcs(mv),
- PositionToFen(pos),
- szShadowPV);
-#endif
+ KEEP_TRACK_OF_FIRST_MOVE_FHs(uLegalMoves == 1);
+ ASSERT(SanityCheckMoves(ctx, x, VERIFY_BEFORE));
+ goto end;
}
- }
-#endif
- UnmakeMove(ctx, mv);
- pf->iCumulativeExtend -= iExtend;
- ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
- if (WE_SHOULD_STOP_SEARCHING)
- {
- iBestScore = iScore;
- goto end;
- }
-
- // Check results of search below this move.
- ASSERT(iBestScore <= iAlpha);
- ASSERT(iAlpha < iBeta);
- if (iScore > iBestScore)
- {
- iBestScore = iScore;
- mvBest = mv;
- pi->mvBest = mv;
-
- if (iScore > iAlpha)
+ else
{
- if (iScore >= iBeta)
- {
#ifdef PERF_COUNTERS
- if (TRUE == fIsLeftoverMove)
- {
- INC(ctx->sCounters.tree.u64LeftoverFH);
- // Gated on fMoveIsPruneReduceEligible now,
- // matching the tries side -- must
- // match exactly, or this would reuse a
- // stale bucket left over from a
- // different (eligible) move for one
- // that never got a bucket computed
- // this iteration.
- if (TRUE == fMoveIsPruneReduceEligible)
- {
- INC(ctx->sCounters.tree.u64EligibleLeftoverFH);
- INC(ctx->sCounters.tree.u64HangEstimateFH[uThisMoveHangBucket]);
- INC(ctx->sCounters.tree.u64GapEstimateFH[uThisMoveGapBucket]);
- INC(ctx->sCounters.tree.u64MoveNumEstimateFH[uThisMoveNumBucket]);
- INC(ctx->sCounters.tree.u64DepthEstimateFH[uThisMoveDepthBucket]);
- INC(ctx->sCounters.tree.u64JointFH
- [uThisMoveDepthBucket][uThisMoveHangBucket]
- [uThisMoveGapBucket][uThisMoveNumBucket]);
- }
- }
-#endif // PERF_COUNTERS
-#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
- if (TRUE == fThisMoveIsCountermoveMatch)
- {
- MOI_INC(ctx->sCounters.tree.u64CMEvidenceFH[uCMEvidenceBucket]);
- }
-#endif
- // Update history and killers list and store in
- // the transposition table.
- UpdateDynamicMoveOrdering(ctx,
- uDepth,
- mv,
- iScore,
- x);
- StoreLowerBound(mv, pos, iScore, uDepth, fThreat);
-
- // A fail-high capturing a non-pawn piece is
- // search-proven evidence that piece was en
- // prise -- but the victim belongs to the
- // *other* side, i.e. whoever is to move at
- // ctx->uPly - 1 (ply parity), not here --
- // "despite the move you're about to make,
- // this piece stays in trouble." Skip near
- // mate: a fail-high there means the whole
- // subtree is winning regardless of this
- // particular piece, not that it was
- // specifically hanging.
- if (mv.pCaptured && !IS_PAWN(mv.pCaptured) &&
- (iBeta < +NMATE))
- {
- ASSERT(ctx->uPly > 0);
- RecordEnprisePieceAtPly(ctx, ctx->uPly - 1,
- mv.cTo);
- }
- KEEP_TRACK_OF_FIRST_MOVE_FHs(uLegalMoves == 1);
- ASSERT(SanityCheckMoves(ctx, x, VERIFY_BEFORE));
- goto end;
- }
- else
+ if (TRUE == fIsLeftoverMove)
{
-#ifdef PERF_COUNTERS
- if (TRUE == fIsLeftoverMove)
- {
- INC(ctx->sCounters.tree.u64LeftoverAlpha);
- }
-#endif
- // PV move...
- UpdatePV(ctx, mv);
- iAlpha = iScore;
+ INC(ctx->sCounters.tree.u64LeftoverAlpha);
}
+#endif
+ // PV move...
+ UpdatePV(ctx, mv);
+ iAlpha = iScore;
}
}
}
}
while(1); // foreach move
+
no_more_moves:
ASSERT(SanityCheckMoves(ctx, x, VERIFY_BEFORE));