summaryrefslogtreecommitdiff
path: root/src/testsee.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/testsee.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/testsee.c')
-rw-r--r--src/testsee.c144
1 files changed, 144 insertions, 0 deletions
diff --git a/src/testsee.c b/src/testsee.c
index ea8318e..6346697 100644
--- a/src/testsee.c
+++ b/src/testsee.c
@@ -375,4 +375,148 @@ TestIsAttackedBB(void)
}
}
}
+
+// SeeFastSign (see.c) is a sign-only shortcut for SEE(pos, mv) >= 0 --
+// GetMoveSee's only consumer (ComputeMoveExtension, searchsup.c) never
+// reads the magnitude, only the sign, so this skips SEE()'s list-
+// build/heap/xray work whenever the defending side has zero attackers
+// on the target square (a case where SEE()'s own exchange loop would
+// terminate on its first iteration anyway). Correctness requirement,
+// per Scott: assert the sign always matches what real SEE() returns --
+// this isn't meant to be an approximation, so any mismatch is a real
+// bug in the fast path, not tolerable noise.
+void
+TestSeeFastSign(void)
+{
+ SEARCHER_THREAD_CONTEXT *ctx;
+ POSITION posBench;
+ MOVE rgmv[MAX_MOVE_STACK];
+ ULONG uMoveCount;
+ ULONG u, x;
+ FLAG fFast, fReal;
+ const ULONG uPositions = 3000;
+
+ Trace("Testing SeeFastSign (sign vs real SEE)...\n");
+ ctx = (SEARCHER_THREAD_CONTEXT *)malloc(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx)
+ {
+ return;
+ }
+
+ for (u = 0; u < uPositions; u++)
+ {
+ GenerateRandomLegalPosition(&posBench);
+ InitializeSearcherContext(&posBench, ctx);
+ GenerateMoves(ctx, (MOVE){0}, GENERATE_DONT_SCORE);
+
+ uMoveCount = 0;
+ for (x = ctx->sMoveStack.uBegin[ctx->uPly];
+ x < ctx->sMoveStack.uEnd[ctx->uPly];
+ x++)
+ {
+ rgmv[uMoveCount++] = ctx->sMoveStack.mvf[x].mv;
+ }
+
+ for (x = 0; x < uMoveCount; x++)
+ {
+ fFast = SeeFastSign(&(ctx->sPosition), rgmv[x]);
+ fReal = (SEE(&(ctx->sPosition), rgmv[x]) >= 0);
+ if (fFast != fReal)
+ {
+ Trace("move=0x%x from=%d to=%d moved=%d captured=%d "
+ "promoted=%d fFast=%d fReal=%d real_see=%d\n",
+ rgmv[x].uMove, rgmv[x].cFrom, rgmv[x].cTo,
+ rgmv[x].pMoved, rgmv[x].pCaptured, rgmv[x].pPromoted,
+ fFast, fReal, SEE(&(ctx->sPosition), rgmv[x]));
+ DumpPosition(&(ctx->sPosition));
+ UtilPanic(TESTCASE_FAILURE,
+ &(ctx->sPosition),
+ "SeeFastSign/SEE sign mismatch",
+ NULL, NULL,
+ __FILE__, __LINE__);
+ }
+ }
+ }
+ free(ctx);
+
+ //
+ // Speed: same isolated, interleaved cycles/call methodology as the
+ // other benchmarks above, real generated moves at each benchmark
+ // position (same setup as TestSEESpeed).
+ //
+ {
+ static const char *rgszFen[3] =
+ {
+ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1",
+ "8/5k2/8/3K4/8/8/8/4R3 w - - 0 1",
+ };
+ static const char *rgszLabel[3] =
+ {
+ "opening ", "middlegame", "endgame ",
+ };
+ SEARCHER_THREAD_CONTEXT *ctx2;
+ POSITION posBench2;
+ MOVE rgmv2[MAX_MOVE_STACK];
+ ULONG uMoveCount2;
+ UINT64 u64RealTotal, u64FastTotal, u64Start;
+ ULONG uIter;
+ FLAG fSink;
+ const ULONG uCallsPerPosition = 200000;
+
+ Trace("Benchmarking SEE sign: real SEE()>=0 vs SeeFastSign "
+ "(interleaved, %lu calls/position)...\n",
+ uCallsPerPosition);
+ ctx2 = (SEARCHER_THREAD_CONTEXT *)malloc(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx2)
+ {
+ return;
+ }
+
+ for (u = 0; u < 3; u++)
+ {
+ FenToPosition(&posBench2, (char *)rgszFen[u]);
+ InitializeSearcherContext(&posBench2, ctx2);
+ GenerateMoves(ctx2, (MOVE){0}, GENERATE_DONT_SCORE);
+
+ uMoveCount2 = 0;
+ for (x = ctx2->sMoveStack.uBegin[ctx2->uPly];
+ x < ctx2->sMoveStack.uEnd[ctx2->uPly];
+ x++)
+ {
+ rgmv2[uMoveCount2++] = ctx2->sMoveStack.mvf[x].mv;
+ }
+ if (uMoveCount2 == 0)
+ {
+ continue;
+ }
+
+ u64RealTotal = 0;
+ u64FastTotal = 0;
+ for (uIter = 0; uIter < uCallsPerPosition; uIter++)
+ {
+ ULONG uPair = uIter % uMoveCount2;
+
+ u64Start = SystemReadTimeStampCounter();
+ fSink = (SEE(&(ctx2->sPosition), rgmv2[uPair]) >= 0);
+ u64RealTotal += (SystemReadTimeStampCounter() - u64Start);
+
+ u64Start = SystemReadTimeStampCounter();
+ fSink = SeeFastSign(&(ctx2->sPosition), rgmv2[uPair]);
+ u64FastTotal += (SystemReadTimeStampCounter() - u64Start);
+ (void)fSink;
+ }
+ printf(" %s: real-SEE %" COMPILER_LONGLONG_UNSIGNED_FORMAT
+ " cycles/call, SeeFastSign %"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call "
+ "(fast is %.2fx real, %u distinct moves)\n",
+ rgszLabel[u],
+ u64RealTotal / uCallsPerPosition,
+ u64FastTotal / uCallsPerPosition,
+ (double)u64FastTotal / (double)u64RealTotal,
+ uMoveCount2);
+ }
+ free(ctx2);
+ }
+}
#endif // TEST