diff options
| author | Scott Gasch <[email protected]> | 2026-09-04 09:22:46 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-04 09:22:46 -0700 |
| commit | be420bb8d1d5d16a4e24ab6fd706a5ae898eaa85 (patch) | |
| tree | 1dff11ecfec08780abfb7e95715ed0f14df1aac5 /src/testsee.c | |
| parent | 6c045be8a37a8eae1ea6aed250944e22af2335a1 (diff) | |
Add bitboard-backed GetAttacks (section 2/3), verified faster than asm
Board-representation migration, sections 2-3 (GetAttacks half):
- board.c: VerifyPositionConsistency's bbPieces consistency check
(migration section 2), verified clean via gmake TEST=1 with the
assert live.
- POSITION.bbPawns[2]: new incrementally-maintained per-color pawn
location bitboard (chess.h), maintained at the same 6 move.c sites
as bbPieces, populated from scratch in fen.c. Distinct from the
pawn-hash-keyed bbPawnLocations; this one needs no
SEARCHER_THREAD_CONTEXT, so it's reachable from GetAttacks's actual
call sites (which only ever have a POSITION*).
- data.c/chess.h/main.c: g_RookRayAll/g_BishopRayAll (all 4 per-square
ray directions pre-ORed) and g_PawnAttackOriginBB[2][128] startup
tables, plus FastFirstBit/FastLastBit (static inline bsf/bsr
wrappers, chess.h) -- supporting tables/helpers for the primitive
below.
- see.c: _WhoAttacksSquareBB (bitboard "who attacks square X" query)
and _GetAttacksBB (SEE_LIST-populating PoC wrapping it), side by
side with the existing SlowGetAttacks/asm GetAttacks -- not wired
into the GetAttacks macro yet (section 6), pure addition.
- testsee.c: SeeListsAreEqual made order-independent (SEE() sorts the
list right after GetAttacks returns, so order was never semantically
significant); TestGetAttacks extended to run _GetAttacksBB as a
third comparison across the existing 20,000-random-position sweep;
added an interleaved asm/Slow/BB cycles-per-call benchmark across
opening/middlegame/endgame positions.
- testsup.c: fixed GenerateRandomLegalPosition (used by the sweep
above) to maintain bbPieces/bbPawns at its two hand-placement sites
-- a latent gap since section 1 that made its own
VerifyPositionConsistency legality gate almost always reject
generated positions, causing large, variable retry-loop slowdowns.
Verified: 20,000-position x every-square x both-colors correctness
sweep passes (gmake TEST=1), precommit_check.sh clean (self-test +
DEBUG smoke test). Benchmark: _GetAttacksBB is ~0.53-0.55x asm
GetAttacks's cycles/call (opening/middlegame) and ~0.89x (endgame) --
faster, not just equivalent, primarily from replacing bbOccupied's
up-to-16-iteration pawn loop with two bbPawns ORs, plus a
g_PawnAttackOriginBB table lookup replacing per-call pawn-delta
arithmetic and per-direction/per-side-group early-outs in the slider
walk. See board_representation/MIGRATION.md section 3 for the full
writeup, including a reverted approach that measured slower and why,
and the CountKingSafetyDefects half's re-scoped (not yet implemented)
design.
Also confirmed (not caused by this work, not fixed here): a
pre-existing non-deterministic MP-race assertion in util.c:1093's PV
printing, reproduced independently on a clean HEAD checkout.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jntky4yGUTyQVaGCXms4F2
Diffstat (limited to 'src/testsee.c')
| -rw-r--r-- | src/testsee.c | 136 |
1 files changed, 125 insertions, 11 deletions
diff --git a/src/testsee.c b/src/testsee.c index da7df59..77fd706 100644 --- a/src/testsee.c +++ b/src/testsee.c @@ -134,16 +134,36 @@ DebugSEE(POSITION *pos, #endif #ifdef TEST -FLAG +static int +_SeeListEntryCompare(const void *pA, const void *pB) +{ + const SEE_THREESOME *a = (const SEE_THREESOME *)pA; + const SEE_THREESOME *b = (const SEE_THREESOME *)pB; + if (a->cLoc != b->cLoc) return ((int)a->cLoc - (int)b->cLoc); + return ((int)a->pPiece - (int)b->pPiece); +} + +FLAG SeeListsAreEqual(SEE_LIST *pA, SEE_LIST *pB) { + // Order-independent: GetAttacks's caller (SEE()) sorts/heaps the + // list immediately after it's populated, so a bitboard-based + // GetAttacks returning the same *set* of attackers in a different + // order is a correct match, not a bug (board_representation/ + // MIGRATION.md section 4). Sort a scratch copy of each by + // (cLoc, pPiece) before comparing field-by-field. + SEE_LIST sA = *pA; + SEE_LIST sB = *pB; ULONG u; - if (pA->uCount != pB->uCount) return FALSE; - for (u = 0; u < pA->uCount; u++) + + if (sA.uCount != sB.uCount) return FALSE; + qsort(sA.data, sA.uCount, sizeof(sA.data[0]), _SeeListEntryCompare); + qsort(sB.data, sB.uCount, sizeof(sB.data[0]), _SeeListEntryCompare); + for (u = 0; u < sA.uCount; u++) { - if ((pA->data[u].pPiece != pB->data[u].pPiece) || - (pA->data[u].cLoc != pB->data[u].cLoc) || - (pA->data[u].uVal != pB->data[u].uVal)) + if ((sA.data[u].pPiece != sB.data[u].pPiece) || + (sA.data[u].cLoc != sB.data[u].cLoc) || + (sA.data[u].uVal != sB.data[u].uVal)) { return FALSE; } @@ -159,20 +179,21 @@ TestGetAttacks(void) COOR c; SEE_LIST rgSlowList; SEE_LIST rgAsmList; + SEE_LIST rgBBList; ULONG color; - + #if !defined(_X86_) && !defined(_X64_) return; #endif - + Trace("Testing GetAttacks...\n"); for (u = 0; u < 20000; u++) { GenerateRandomLegalPosition(&pos); - FOREACH_SQUARE(c) + FOREACH_SQUARE(c) { if (!IS_ON_BOARD(c)) continue; - for (color = BLACK; color <= WHITE; color++) + for (color = BLACK; color <= WHITE; color++) { SlowGetAttacks(&rgSlowList, &pos, @@ -185,11 +206,104 @@ TestGetAttacks(void) if (!SeeListsAreEqual(&rgSlowList, &rgAsmList)) { UtilPanic(TESTCASE_FAILURE, - &pos, + &pos, "SEE_LIST mismatch", &rgSlowList, &rgAsmList, __FILE__, __LINE__); } + + // board_representation/MIGRATION.md section 3/4: + // bbPieces-backed GetAttacks PoC, same correctness + // gate as the asm/C comparison above. + _GetAttacksBB(&rgBBList, + &pos, + c, + color); + if (!SeeListsAreEqual(&rgSlowList, &rgBBList)) + { + UtilPanic(TESTCASE_FAILURE, + &pos, + "SEE_LIST mismatch (_GetAttacksBB)", + &rgSlowList, &rgBBList, + __FILE__, __LINE__); + } + } + } + } + + // + // Speed: board_representation/MIGRATION.md section 5's isolated + // cycles/call microbenchmark, pulled forward here since it's cheap + // to add right alongside the correctness gate that just proved the + // two implementations equivalent. Three positions spanning piece + // density (opening/middlegame/endgame), SlowGetAttacks vs + // _GetAttacksBB interleaved call-by-call (not phase-by-phase) to + // cancel shared-box noise -- a red flag (flat or inverted result) + // here would mean stopping before wiring this in any further, same + // as the Eval occupancy-bitboard work that motivated this file. + { + 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; + SEE_LIST rgList; + UINT64 u64SlowTotal, u64AsmTotal, u64BBTotal, u64Start; + ULONG uIter; + ULONG uSq; + COOR cBench; + ULONG uSide; + const ULONG uCallsPerPosition = 200000; + + // GetAttacks (unqualified) is the real production entry point -- + // the hand-tuned x86/x64 asm routine, not SlowGetAttacks (the C + // reference used only for correctness comparison above). That's + // the actual competitor _GetAttacksBB has to beat; SlowGetAttacks + // is included only as a third data point, not the bar to clear. + Trace("Benchmarking GetAttacks: asm GetAttacks vs SlowGetAttacks " + "vs _GetAttacksBB (interleaved, %lu calls/position)...\n", + uCallsPerPosition); + for (u = 0; u < 3; u++) + { + FenToPosition(&posBench, (char *)rgszFen[u]); + u64SlowTotal = 0; + u64AsmTotal = 0; + u64BBTotal = 0; + for (uIter = 0; uIter < uCallsPerPosition; uIter++) + { + uSq = uIter % 64; + cBench = BIT_NUMBER_TO_COOR(uSq); + uSide = uIter & 1; + if (!IS_ON_BOARD(cBench)) continue; + + u64Start = SystemReadTimeStampCounter(); + GetAttacks(&rgList, &posBench, cBench, uSide); + u64AsmTotal += (SystemReadTimeStampCounter() - u64Start); + + u64Start = SystemReadTimeStampCounter(); + SlowGetAttacks(&rgList, &posBench, cBench, uSide); + u64SlowTotal += (SystemReadTimeStampCounter() - u64Start); + + u64Start = SystemReadTimeStampCounter(); + _GetAttacksBB(&rgList, &posBench, cBench, uSide); + u64BBTotal += (SystemReadTimeStampCounter() - u64Start); } + printf(" %s: asm GetAttacks %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " cycles/call, SlowGetAttacks %" + COMPILER_LONGLONG_UNSIGNED_FORMAT + " cycles/call, _GetAttacksBB %" + COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call " + "(BB is %.2fx asm)\n", + rgszLabel[u], + u64AsmTotal / uCallsPerPosition, + u64SlowTotal / uCallsPerPosition, + u64BBTotal / uCallsPerPosition, + (double)u64BBTotal / (double)u64AsmTotal); } } } |
