summaryrefslogtreecommitdiff
path: root/src/see.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-18 22:35:42 -0700
committerScott Gasch <[email protected]>2026-09-18 22:35:42 -0700
commit91dc4a9393204104c06fa64a05be24d08feca0ef (patch)
tree26933246f26a5acfe0590fe678bb805de4d9af1a /src/see.c
parent73b82a01c6f1b15489608c7e2cf757180746f7b8 (diff)
Add SeeFastSign, a provably-exact sign-only shortcut for SEE(), and wire it into GetMoveSee's cache-miss path
GetMoveSee's only consumer, ComputeMoveExtension, reads iMoveSee in exactly two places -- both plain sign checks (`< 0` / `>= 0`), never the magnitude. On a cache miss GetMoveSee always paid for a full SEE() exchange walk (list-build, heap, x-ray discovery) just to throw away everything but the sign. SeeFastSign answers the sign question directly, without building either SEE_LIST: if the defending side has zero attackers on the target square, SEE()'s own exchange loop terminates on its first iteration anyway and returns exactly captured+promoted, which is never negative -- so "no defender" provably implies SEE >= 0, not an approximation. Falls back to real SEE() whenever a defender exists. Must check with mv.cFrom removed from occupancy, not the raw pre-move board -- the mover's own departure can unmask a defender that's currently blocked by the mover itself sitting on the same ray (exactly what SEE()'s own _AddXRaysMailbox discovers explicitly). A first version missed this and returned a wrong TRUE for a quiet bishop move that unmasked a queen's diagonal attack through the vacated square, caught by the correctness test below before it ever reached production code. testsee.c's TestSeeFastSign asserts SeeFastSign's sign matches real SEE() across 3000 random positions x every legal move, then benchmarks both: SeeFastSign runs at 0.40x-0.65x real SEE()'s cycles/call. Measured end-to-end (sd10 + sn5M, all three curated suites, before vs. after): node counts are bit-for-bit identical on every suite/depth pair, as expected from a provably sign-equivalent function -- zero search-behavior change, no self-play gate needed. Wall-clock improved on all 6 measurements, ~2.7% overall. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/see.c')
-rw-r--r--src/see.c75
1 files changed, 75 insertions, 0 deletions
diff --git a/src/see.c b/src/see.c
index 47efd30..2edd6c5 100644
--- a/src/see.c
+++ b/src/see.c
@@ -933,6 +933,81 @@ Return value:
}
+FLAG
+SeeFastSign(IN POSITION *pos,
+ IN MOVE mv)
+/**
+
+Routine description:
+
+ Cheap answer to "is SEE(pos, mv) >= 0", for callers that only need
+ the sign -- GetMoveSee's only consumer, ComputeMoveExtension, reads
+ iMoveSee in exactly two places (searchsup.c), both pure sign checks
+ (`iMoveSee < 0` / `iMoveSee >= 0`), never the magnitude. Real SEE()
+ unconditionally builds both sides' full sorted/heaped attacker
+ lists and discovers x-rays before playing out the exchange, even
+ though the exchange loop's own first iteration already terminates
+ immediately whenever the defending side has zero attackers on
+ cTo -- _MinLegalPiece finds nothing, breaks, and SEE() returns
+ exactly rgiList[0] = PIECE_VALUE(mv.pCaptured) +
+ PIECE_VALUE(mv.pPromoted), which is always >= 0 (a piece value is
+ never negative). So "defender list is empty" provably implies
+ SEE(pos, mv) >= 0 -- not an approximation of the sign, the actual
+ sign, derived without ever building either SEE_LIST, running the
+ heap, or discovering a single x-ray.
+
+ Checks defender existence via two raw bitboard tests (the same
+ ones _GetAttacksBB would populate a full list from: pawns via
+ g_PawnAttackOriginBB, everything else via _WhoAttacksSquareBB) --
+ falls back to the real SEE() whenever a defender is actually
+ present, since the sign genuinely depends on the full exchange at
+ that point and there's no cheap shortcut left to take.
+
+ Must check with mv.cFrom removed from occupancy, not the raw
+ pre-move board: the mover's own departure can unmask a defender
+ that's currently blocked by the mover itself sitting on the same
+ ray between cTo and the defender (exactly what real SEE()'s
+ _AddXRaysMailbox discovers explicitly, right before its exchange
+ loop starts). A first version of this function used the raw
+ pos->bbOccupied and returned a wrong TRUE for a quiet bishop move
+ that unmasked a queen's own diagonal attack through the vacated
+ square -- caught by TestSeeFastSign's correctness check (real
+ SEE() said -300, this said nonnegative). Pawn attacks don't need
+ this adjustment -- a pawn's attack squares are a fixed 2-square
+ delta, never ray-blocked by anything to begin with.
+
+Parameters:
+
+ POSITION *pos,
+ MOVE mv
+
+Return value:
+
+ FLAG : TRUE if SEE(pos, mv) >= 0
+
+**/
+{
+ ULONG uDefender = FLIP(GET_COLOR(mv.pMoved));
+ BITBOARD bbOccupiedWithoutMover = pos->bbOccupied & ~COOR_TO_BB(mv.cFrom);
+
+ if (g_PawnAttackOriginBB[uDefender][mv.cTo] & pos->bbPawns[uDefender])
+ {
+ return (SEE(pos, mv) >= 0);
+ }
+ if (_WhoAttacksSquareBB(pos, mv.cTo, uDefender, bbOccupiedWithoutMover))
+ {
+ return (SEE(pos, mv) >= 0);
+ }
+
+ //
+ // No defender at all -- the exchange can't continue past our own
+ // move, so the result is exactly the material gained (captured +
+ // promoted), which is never negative.
+ //
+ return TRUE;
+}
+
+
SCORE
SEE(IN POSITION *pos,
IN MOVE mv)