diff options
| author | Scott Gasch <[email protected]> | 2026-09-18 23:43:55 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-18 23:43:55 -0700 |
| commit | b70be6fde0fef0694e404e16d70f00c73dd971a2 (patch) | |
| tree | d5523636bf76bd53f8ac4a60e508c458fa3c2a51 | |
| parent | 004a5736c6d41747d8fc0e15d230ab80bd6cced6 (diff) | |
see.c: replace _WhoAttacksSquareBB's ray-walk with magic-bitboard lookups
_RookAttacksBB/_BishopAttacksBB (generate.c) are already O(1)
FORCEINLINE magic-table lookups, proven and already used everywhere
else in this codebase (eval.c, searchsup.c, generate.c's own move
generation) -- this function alone predated that migration and was
left on a manual per-direction ray-walk (4 rook + 4 bishop directions,
nearest-blocker isolation via FastFirstBit/FastLastBit). King handling
is also branchless now: king adjacency is symmetric, so
g_KingAttacksBB[cSquare] doubles as "every square a king standing
there would attack cSquare from," same O(1)-table-lookup shape the
knight line already uses -- no DISTANCE()==1 branch needed.
Old ray-walk kept as _WhoAttacksSquareRayBB, testsee.c's
TestWhoAttacksSquareMagic correctness-diff reference (20000 random
positions x every square x both colors, clean) -- same convention as
_AddXRaysMailbox next to _AddXRaysBB from an earlier SEE investigation
this session.
Isolated benchmark: magic is 0.95x-0.99x ray-walk in a standalone
3-position microbenchmark (small, since the ray-walk already has its
own early-out for "no slider anywhere near this square," per its own
header comment). Measured end-to-end (sd10 + sn5M, all three curated
suites, before vs. after): node counts and solve counts bit-for-bit
identical (pure algorithmic swap, zero behavior change). Wall-clock
~0.8% faster overall, all 6 measurements faster, none slower.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
| -rwxr-xr-x | src/chess.h | 22 | ||||
| -rw-r--r-- | src/main.c | 1 | ||||
| -rw-r--r-- | src/see.c | 95 | ||||
| -rw-r--r-- | src/testsee.c | 105 |
4 files changed, 208 insertions, 15 deletions
diff --git a/src/chess.h b/src/chess.h index 802c89d..e98c99f 100755 --- a/src/chess.h +++ b/src/chess.h @@ -3442,6 +3442,9 @@ TestIsAttackedBB(void); void TestSeeFastSign(void); +void +TestWhoAttacksSquareMagic(void); + // // hash.c // @@ -3954,17 +3957,26 @@ _GetAttacksBB(SEE_LIST *pList, COOR cSquare, ULONG uSide); -// board_representation/MOVEGEN_MIGRATION.md section 6a: exposed (not -// static) so generate.c's Part B king-flight code can call it with a -// king-vacated occupancy bitboard. Deliberately excludes pawns -- see -// its own header comment in see.c; callers needing pawn attacks must -// check g_PawnAttackOriginBB separately. +// Magic-bitboard implementation (as of 2026-09-19) -- exposed (not +// static) so generate.c's Part B king-flight code and movesup.c can +// call it with a king-vacated occupancy bitboard. Deliberately +// excludes pawns -- see its own header comment in see.c; callers +// needing pawn attacks must check g_PawnAttackOriginBB separately. BITBOARD _WhoAttacksSquareBB(POSITION *pos, COOR cSquare, ULONG uSide, BITBOARD bbOccupied); +// Ray-walk reference implementation, superseded by the magic version +// above -- see its own header comment in see.c. Exposed non-static so +// testsee.c can diff it against the magic version. +BITBOARD +_WhoAttacksSquareRayBB(POSITION *pos, + COOR cSquare, + ULONG uSide, + BITBOARD bbOccupied); + // _GetAttacksBB is the only implementation as of 2026-09-06 (see // MIGRATION.md section 7) -- this macro exists only so call sites // written against the name "GetAttacks" didn't need touching when the @@ -616,6 +616,7 @@ Return value: TestGetAttacks(); TestIsAttackedBB(); TestSeeFastSign(); + TestWhoAttacksSquareMagic(); TestMoveGenerator(); TestLegalMoveGenerator(); TestGenerateKnightSpeed(); @@ -47,17 +47,24 @@ Revision History: // EVAL.md section 0. Callers below just read pos->bbOccupied.) // -// Non-static (unlike its historical file-local status) so -// generate.c's Part B (_GenerateEscapes) bitboard work can call it -// directly for king-flight safety testing, passing an occupancy -// bitboard with the king itself removed to correctly account for -// x-ray/discovered attacks when the king steps out of a slider's way -// -- see board_representation/MOVEGEN_MIGRATION.md section 6a. +// Ray-walk reference implementation -- superseded 2026-09-19 by the +// magic-bitboard version below (_WhoAttacksSquareBB itself now uses +// magic lookups; this name is kept only as testsee.c's +// TestWhoAttacksSquareMagic correctness-diff reference, same +// convention as _AddXRaysMailbox next to _AddXRaysBB). Non-static +// (unlike its historical file-local status) so generate.c's Part B +// (_GenerateEscapes) bitboard work could call it directly for +// king-flight safety testing, passing an occupancy bitboard with the +// king itself removed to correctly account for x-ray/discovered +// attacks when the king steps out of a slider's way -- see +// board_representation/MOVEGEN_MIGRATION.md section 6a. (That real +// call site, and _GetAttacksBB's, now go through the magic version +// below instead.) BITBOARD -_WhoAttacksSquareBB(IN POSITION *pos, - IN COOR cSquare, - IN ULONG uSide, - IN BITBOARD bbOccupied) +_WhoAttacksSquareRayBB(IN POSITION *pos, + IN COOR cSquare, + IN ULONG uSide, + IN BITBOARD bbOccupied) /** Routine description: @@ -183,6 +190,74 @@ Return value: return bbAttackers; } +// Magic-bitboard implementation -- the real _WhoAttacksSquareBB as of +// 2026-09-19, all production callers (movesup.c, generate.c, +// _GetAttacksBB/SeeFastSign below) go through this name. +// _RookAttacksBB/_BishopAttacksBB (generate.c) are already O(1) +// FORCEINLINE magic-table lookups (one multiply, one shift, one table +// read), proven and already used everywhere else in this codebase +// (eval.c, searchsup.c, generate.c's own move generation) -- this +// function alone predated that migration and was left on the manual +// per-direction ray-walk (_WhoAttacksSquareRayBB above, kept as +// testsee.c's correctness-diff reference). King handling is branchless: +// king adjacency is symmetric, so g_KingAttacksBB[cSquare] (every +// square one king-step from cSquare) doubles as "every square a king +// standing there would attack cSquare from" -- ANDing it against a +// single-bit bitboard of the actual king square is either that bit or +// zero, same O(1)-table-lookup shape the knight line already uses, no +// DISTANCE()==1 branch needed. +BITBOARD +_WhoAttacksSquareBB(IN POSITION *pos, + IN COOR cSquare, + IN ULONG uSide, + IN BITBOARD bbOccupied) +/** + +Routine description: + + Return a bitboard of every uSide knight/bishop/rook/queen/king + that attacks cSquare in the current position, blockers included. + Pawns are deliberately excluded -- see _GetAttacksBB. Magic- + bitboard equivalent of the historical ray-walk implementation + (_WhoAttacksSquareRayBB above) -- see that function's description + for what this replaces. + +Parameters: + + POSITION *pos, + COOR cSquare : target square + ULONG uSide : side whose attackers on cSquare we want + BITBOARD bbOccupied : full-board occupancy (see pos->bbOccupied) + +Return value: + + BITBOARD + +**/ +{ + BITBOARD bbAttackers; + BITBOARD bbRookSliders; + BITBOARD bbBishopSliders; + + // Branchless: king adjacency is symmetric, so g_KingAttacksBB[cSquare] + // (every square one king-step from cSquare) doubles as "every square + // a king standing there would attack cSquare from" -- ANDing it + // against a single-bit bitboard of the actual king square is either + // that bit or zero, exactly matching what the DISTANCE()==1 branch + // computed, same O(1)-table-lookup shape the knight line above + // already uses. + bbAttackers = g_KnightAttacksBB[cSquare] & pos->bbPieces[uSide][KNIGHT]; + bbAttackers |= g_KingAttacksBB[cSquare] & COOR_TO_BB(KingPosition(pos, uSide)); + + bbRookSliders = pos->bbPieces[uSide][ROOK] | pos->bbPieces[uSide][QUEEN]; + bbAttackers |= _RookAttacksBB(cSquare, bbOccupied) & bbRookSliders; + + bbBishopSliders = pos->bbPieces[uSide][BISHOP] | pos->bbPieces[uSide][QUEEN]; + bbAttackers |= _BishopAttacksBB(cSquare, bbOccupied) & bbBishopSliders; + + return bbAttackers; +} + void CDECL _GetAttacksBB(IN OUT SEE_LIST *pList, IN POSITION *pos, diff --git a/src/testsee.c b/src/testsee.c index 6346697..bbcd7e0 100644 --- a/src/testsee.c +++ b/src/testsee.c @@ -519,4 +519,109 @@ TestSeeFastSign(void) free(ctx2); } } + +// _WhoAttacksSquareMagicBB (see.c) replaces _WhoAttacksSquareBB's +// manual per-direction ray-walk with the same O(1) magic-bitboard +// lookups (_RookAttacksBB/_BishopAttacksBB, generate.c) already used +// everywhere else in this codebase. Same correctness+speed pattern as +// the other Test* functions here. +void +TestWhoAttacksSquareMagic(void) +{ + POSITION pos; + ULONG u; + COOR c; + ULONG uSide; + BITBOARD bbRay; + BITBOARD bbMagic; + const ULONG uPositions = 20000; + + Trace("Testing _WhoAttacksSquareMagicBB (magic vs ray-walk)...\n"); + for (u = 0; u < uPositions; u++) + { + GenerateRandomLegalPosition(&pos); + FOREACH_SQUARE(c) + { + if (!IS_ON_BOARD(c)) continue; + for (uSide = BLACK; uSide <= WHITE; uSide++) + { + bbRay = _WhoAttacksSquareRayBB(&pos, c, uSide, pos.bbOccupied); + bbMagic = _WhoAttacksSquareBB(&pos, c, uSide, + pos.bbOccupied); + if (bbRay != bbMagic) + { + Trace("cSquare=%d uSide=%lu bbRay=0x%llx bbMagic=0x%llx\n", + c, uSide, bbRay, bbMagic); + UtilPanic(TESTCASE_FAILURE, + &pos, + "_WhoAttacksSquareBB/ray-walk mismatch", + NULL, NULL, + __FILE__, __LINE__); + } + } + } + } + + // + // Speed: same isolated, interleaved cycles/call methodology as the + // other benchmarks above. + // + { + 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 ", + }; + POSITION posBench; + BITBOARD bbSink; + UINT64 u64RayTotal, u64MagicTotal, u64Start; + ULONG uIter; + ULONG uSq; + COOR cBench; + ULONG uSideBench; + const ULONG uCallsPerPosition = 200000; + + Trace("Benchmarking _WhoAttacksSquareBB: ray-walk vs magic " + "(interleaved, %lu calls/position)...\n", + uCallsPerPosition); + for (u = 0; u < 3; u++) + { + FenToPosition(&posBench, (char *)rgszFen[u]); + u64RayTotal = 0; + u64MagicTotal = 0; + for (uIter = 0; uIter < uCallsPerPosition; uIter++) + { + uSq = uIter % 64; + cBench = BIT_NUMBER_TO_COOR(uSq); + uSideBench = uIter & 1; + if (!IS_ON_BOARD(cBench)) continue; + + u64Start = SystemReadTimeStampCounter(); + bbSink = _WhoAttacksSquareRayBB(&posBench, cBench, uSideBench, + posBench.bbOccupied); + u64RayTotal += (SystemReadTimeStampCounter() - u64Start); + + u64Start = SystemReadTimeStampCounter(); + bbSink = _WhoAttacksSquareBB(&posBench, cBench, + uSideBench, + posBench.bbOccupied); + u64MagicTotal += (SystemReadTimeStampCounter() - u64Start); + (void)bbSink; + } + printf(" %s: ray-walk %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " cycles/call, magic %" + COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call " + "(magic is %.2fx ray-walk)\n", + rgszLabel[u], + u64RayTotal / uCallsPerPosition, + u64MagicTotal / uCallsPerPosition, + (double)u64MagicTotal / (double)u64RayTotal); + } + } +} #endif // TEST |
