From 57502d6e205bc802fc9b89fd1534add5efc4b9ea Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 5 Sep 2026 00:52:13 -0700 Subject: Retire pawns' bvAttacks writes in favor of pos->bbPawnAttacks[2] First piece type converted per board_representation/EVAL.md section 2: pawns no longer write into rgSquare[c|8].bvAttacks at all. Every consumer of "does a pawn attack this square" now reads pos->bbPawnAttacks[2] directly -- a plain bitboard, computed fresh each Eval() call from pos->bbPawns[] via the same shift-and-mask technique generate.c's _GenerateAllPawnMovesBB already uses (zero per-pawn mailbox iteration, vs. up to 16 delta+IS_ON_BOARD checks before). Knight/bishop/rook/queen/king are unchanged -- still populate/read their own bvAttacks bits (uMinor/uRook/uQueen/uKing) the old way until their own conversions land. Consumer changes, all in eval.c: - UNSAFE_FOR_MINOR retired as a macro (it only ever tested the pawn bit) -- its 3 call sites (knight, bishop x2) now test bbPawnAttacks directly. - UNSAFE_FOR_ROOK/_QUEEN masks narrowed to drop the now-dead pawn bit; call sites OR in an explicit bbPawnAttacks test alongside the narrowed bvAttacks read. - Two direct .small.uPawn reads (bishop's transient-pawn mobility credit, bishop's defended-pawn bonus) switched to bbPawnAttacks tests. - _EvalKing's bvAttack/bvDefend (the real king-danger computation) OR the bitboard bit back in at both read points, careful to preserve the original ordering where bvDefend must reflect the king's own just-set defend bit. - _WhoControlsSquareFast (used by passer-race/trapped-piece/danger code) ORs the bitboard bit back into its g_SwapTable index at the same bit position PAWN_BIT always occupied. - Two bugs caught by manually auditing every remaining |8 site after the fact (not by any test failing): _EvalPawns' own pawn-duo and backward-pawn detection read bvAttacks.uWholeThing at a point in Eval()'s sequence where only pawns could have written it -- once pawns stopped writing there, both checks went permanently dead silently. Fixed to read bbPawnAttacks directly. No self-test caught this; it's exactly the gap EVAL.md section 5's planned exact-score harness is meant to close. EVAL_TIME instrumentation: split _EvalPawns' cycle counter into pawn-hash hit/miss buckets (chess.h/eval.c/root.c), answering whether the hash is still worth it now that attack-bit population is nearly free. Measured on one sd12 benchmark position: hits average 90.4 cycles, misses average 1741.3 cycles (~19x), 97.15% hit rate -- the hash stays a clear win; the miss cost was never mostly attack-bit population (that's a separate ~1% bucket now, down from ~3.6%), it's the isolated/doubled/duo/backward-pawn scoring loops, which still do real per-pawn work on a miss. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj --- src/eval.c | 209 +++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 113 insertions(+), 96 deletions(-) (limited to 'src/eval.c') diff --git a/src/eval.c b/src/eval.c index 17793da..35ee663 100755 --- a/src/eval.c +++ b/src/eval.c @@ -1176,10 +1176,21 @@ Return value: **/ { + // + // bvAttacks no longer carries the pawn bit (pawns write + // pos->bbPawnAttacks[2] directly instead, see + // _PopulatePawnAttackBits) -- OR it back in at its usual bit + // position (PAWN_BIT) so g_SwapTable's indexing below sees the + // same bit shape it always has. + // ULONG uWhite = pos->rgSquare[c|8].bvAttacks[WHITE].uSmall | - pos->rgSquare[c|8].bvAttacks[WHITE].uXray; + pos->rgSquare[c|8].bvAttacks[WHITE].uXray | + ((pos->bbPawnAttacks[WHITE] & COOR_TO_BB(c)) ? + PAWN_BIT : 0); ULONG uBlack = pos->rgSquare[c|8].bvAttacks[BLACK].uSmall | - pos->rgSquare[c|8].bvAttacks[BLACK].uXray; + pos->rgSquare[c|8].bvAttacks[BLACK].uXray | + ((pos->bbPawnAttacks[BLACK] & COOR_TO_BB(c)) ? + PAWN_BIT : 0); ULONG u; PIECE p; CHAR ch; @@ -1763,9 +1774,30 @@ Return value: } -#define PAWN_ATTACK_BLACK_DELTA (+15) -#define PAWN_ATTACK_WHITE_DELTA (-17) - +// board_representation/EVAL.md section 2/0d: bitboard replacement for +// the old per-pawn mailbox delta+IS_ON_BOARD population this function +// used to do. pos->bbPawns[2] already has every pawn location +// (incrementally maintained, chess.h) with zero per-pawn iteration +// needed to query it -- the exact shift-and-mask technique +// generate.c's _GenerateAllPawnMovesBB already uses to bulk-generate a +// whole side's pawn captures (its bbCapLeft/bbCapRight, before being +// masked down to actual enemy-occupied squares) is exactly "every +// square this side's pawns attack" -- reused here verbatim, minus the +// enemy-occupancy mask, since attack-bit population doesn't care what +// (if anything) sits on the attacked square. See that function's +// header comment for the full square-numbering/shift-direction +// derivation (WHITE forward = bb >> 8, BLACK forward = bb << 8, +// diagonals need the *opposite* file excluded to prevent same-row +// wraparound) -- not repeated here. +// +// Unlike the old version, this does NOT write PAWN_BIT into +// rgSquare[c|8].bvAttacks -- pos->bbPawnAttacks[2] (chess.h) is now +// the single source of truth for "does a pawn attack this square", +// read directly by every consumer (UNSAFE_FOR_MINOR's old callers, +// UNSAFE_FOR_ROOK/_QUEEN, _EvalKing's bvAttack/bvDefend). Knight/ +// bishop/rook/queen/king still populate/read the rest of bvAttacks +// (uMinor/uRook/uQueen/uKing) the old way, so _ClearAttackTables(pos) +// still needs to run here first. static void _PopulatePawnAttackBits(IN OUT POSITION *pos) /** @@ -1784,81 +1816,15 @@ Return value: **/ { - ULONG u; - COOR c; - COOR cAttack; - - // - // IDEA: combine clearing and initial population somehow? - // - // r - - - - - - l : only check L- and R- - // r - - - - - - l - // R A A A A A A L - // R A A A A A A L - // R A A A A A A L - // R A A A A A A L - // r - - - - - - l - // r - - - - - - l : only check L+ and R+ _ClearAttackTables(pos); - ASSERT(pos->uPawnCount[BLACK] <= 8); - for (u = 0; - u < pos->uPawnCount[BLACK]; - u++) - { - c = pos->cPawns[BLACK][u]; - ASSERT(IS_ON_BOARD(c)); - ASSERT(pos->rgSquare[c].pPiece); - ASSERT(IS_PAWN(pos->rgSquare[c].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == BLACK); - - // - // IDEA: These IS_ON_BOARD checks can be removed by a lookup table. - // - cAttack = c + PAWN_ATTACK_BLACK_DELTA; - if (IS_ON_BOARD(cAttack)) - { - ASSERT((cAttack + 8) == (cAttack|8)); - ASSERT(!IS_ON_BOARD(cAttack|8)); - pos->rgSquare[cAttack|8].bvAttacks[BLACK].small.uPawn = 1; - } - cAttack += 2; - if (IS_ON_BOARD(cAttack)) - { - ASSERT((cAttack + 8) == (cAttack|8)); - cAttack |= 8; - ASSERT(!IS_ON_BOARD(cAttack)); - pos->rgSquare[cAttack].bvAttacks[BLACK].small.uPawn = 1; - } - } - - ASSERT(pos->uPawnCount[WHITE] <= 8); - for (u = 0; - u < pos->uPawnCount[WHITE]; - u++) - { - c = pos->cPawns[WHITE][u]; - ASSERT(IS_ON_BOARD(c)); - ASSERT(pos->rgSquare[c].pPiece); - ASSERT(IS_PAWN(pos->rgSquare[c].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == WHITE); + pos->bbPawnAttacks[BLACK] = + ((pos->bbPawns[BLACK] & ~BBFILE[0]) << 7) | + ((pos->bbPawns[BLACK] & ~BBFILE[7]) << 9); - cAttack = c + PAWN_ATTACK_WHITE_DELTA; - if (IS_ON_BOARD(cAttack)) - { - ASSERT((cAttack + 8) == (cAttack|8)); - ASSERT(!IS_ON_BOARD(cAttack|8)); - pos->rgSquare[cAttack|8].bvAttacks[WHITE].small.uPawn = 1; - } - cAttack += 2; - if (IS_ON_BOARD(cAttack)) - { - ASSERT((cAttack + 8) == (cAttack|8)); - cAttack |= 8; - ASSERT(!IS_ON_BOARD(cAttack)); - pos->rgSquare[cAttack].bvAttacks[WHITE].small.uPawn = 1; - } - } + pos->bbPawnAttacks[WHITE] = + ((pos->bbPawns[WHITE] & ~BBFILE[0]) >> 9) | + ((pos->bbPawns[WHITE] & ~BBFILE[7]) >> 7); } @@ -2170,8 +2136,16 @@ Return value: // cSquare = c + d1; ASSERT(IS_ON_BOARD(cSquare)); - ASSERT((cSquare + 8) == (cSquare | 8)); - if (pos->rgSquare[cSquare|8].bvAttacks[uColor].uWholeThing) + // + // At this point in Eval()'s sequence (inside _EvalPawns, + // before any other piece type has run this call), pawns + // are the only thing that could possibly have marked an + // attack -- bvAttacks itself no longer carries the pawn + // bit at all (see _PopulatePawnAttackBits), so this reads + // pos->bbPawnAttacks directly instead of the (permanently + // zero, at this point in execution) bvAttacks word. + // + if (pos->bbPawnAttacks[uColor] & COOR_TO_BB(cSquare)) { // // Count pawn duos. See "Pawn Power in Chess" pp 10-16 @@ -2193,7 +2167,7 @@ Return value: (FLIP(uColor) * (9 - RANK(cSquare)))); ASSERT(iDuos[uColor] <= (7 * 8)); } - else if (pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)].uWholeThing) + else if (pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) { // // Detect backwards pawns. See "Pawn Power in Chess" pp 25-27 @@ -2665,7 +2639,7 @@ Return value: ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == FLIP(uColor)); ASSERT(pos->bbPawns[FLIP(uColor)] & COOR_TO_BB(cSquare)); i += - (pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)].small.uPawn != 0) * + ((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) != 0) * TRANSIENT_PAWN_ON_BISHOP_COLOR[cSquare] / 2; } EVAL_TERM(uColor, @@ -2723,13 +2697,13 @@ Return value: { case BMOB_EMPTY: uCurrentMobility += - !UNSAFE_FOR_MINOR(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)]); + !(pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)); fStop = FALSE; break; case BMOB_ENEMY_PAWN: uCurrentMobility += - !UNSAFE_FOR_MINOR(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)]); + !(pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)); fStop = TRUE; break; @@ -2824,7 +2798,7 @@ Return value: if (TRUE == _IsSquareSafeFromEnemyPawn(pos, c, bb)) { // Defended (and defending) a friendly pawn. - if (pos->rgSquare[c|8].bvAttacks[uColor].small.uPawn) + if (pos->bbPawnAttacks[uColor] & COOR_TO_BB(c)) { #ifdef DEBUG p = BLACK_PAWN | uColor; @@ -3097,7 +3071,7 @@ Return value: { case NMOB_MOBILE_SQUARE: uMobilitySquares += - !UNSAFE_FOR_MINOR(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)]); + !(pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)); break; case NMOB_ENEMY_OTHER: @@ -3406,13 +3380,15 @@ Return value: { case RMOB_EMPTY: uCurrentMobility += - !UNSAFE_FOR_ROOK(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)]); + !((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) || + UNSAFE_FOR_ROOK(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)])); fStop = FALSE; break; case RMOB_ENEMY_LESS: uCurrentMobility += - !UNSAFE_FOR_ROOK(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)]); + !((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) || + UNSAFE_FOR_ROOK(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)])); fStop = TRUE; break; @@ -3707,13 +3683,15 @@ Return value: { case QMOB_EMPTY: uTotalMobility += - !UNSAFE_FOR_QUEEN(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)]); + !((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) || + UNSAFE_FOR_QUEEN(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)])); fStop = FALSE; break; case QMOB_ENEMY_LESS: uTotalMobility += - !UNSAFE_FOR_QUEEN(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)]); + !((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) || + UNSAFE_FOR_QUEEN(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)])); fStop = TRUE; break; @@ -3943,11 +3921,30 @@ Return value: { p = pos->rgSquare[cSquare].pPiece; - cSquare |= 8; - bvAttack = pos->rgSquare[cSquare].bvAttacks[ufColor].uSmall; - bvXray = pos->rgSquare[cSquare].bvAttacks[ufColor].uXray; - pos->rgSquare[cSquare].bvAttacks[uColor].small.uKing = 1; - bvDefend = pos->rgSquare[cSquare].bvAttacks[uColor].uSmall; + // + // bvAttacks no longer carries the pawn bit (pawns write + // pos->bbPawnAttacks[2] directly instead, see + // _PopulatePawnAttackBits) -- OR it back in here from the + // bitboard, keyed off the real board square (cSquare, + // before the |8 below flips it into the invisible-half + // storage index bvAttacks itself uses). bvDefend's OR-in + // (and the .small.uKing = 1 write) must stay after this + // point, same order as before -- bvDefend is meant to + // include this king's own just-set attack/defend bit on + // the square. + // + bvAttack = pos->rgSquare[cSquare|8].bvAttacks[ufColor].uSmall | + ((pos->bbPawnAttacks[ufColor] & COOR_TO_BB(cSquare)) ? + PAWN_BIT : 0); + { + COOR cRealSquare = cSquare; + cSquare |= 8; + bvXray = pos->rgSquare[cSquare].bvAttacks[ufColor].uXray; + pos->rgSquare[cSquare].bvAttacks[uColor].small.uKing = 1; + bvDefend = pos->rgSquare[cSquare].bvAttacks[uColor].uSmall | + ((pos->bbPawnAttacks[uColor] & COOR_TO_BB(cRealSquare)) ? + PAWN_BIT : 0); + } // // Count squares near the king the enemy queen specifically @@ -4850,8 +4847,28 @@ Return value: // save some time. BEFORE ANY CODE BELOW TOUCHES THE ATTACK // TABLES IT NEEDS TO CLEAR/POPULATE THEM THOUGH!!! // - TIMED_EVAL_CALL(ctx, u64CyclesEvalPawns, - pHash = _EvalPawns(ctx, &fDeferred)); +#ifdef EVAL_TIME + { + UINT64 u64PawnTimer = SystemReadTimeStampCounter(); + pHash = _EvalPawns(ctx, &fDeferred); + { + UINT64 u64Elapsed = SystemReadTimeStampCounter() - u64PawnTimer; + ctx->sCounters.tree.u64CyclesEvalPawns += u64Elapsed; + if (fDeferred) + { + ctx->sCounters.tree.u64CyclesEvalPawnsHit += u64Elapsed; + ctx->sCounters.tree.u64CountEvalPawnsHit++; + } + else + { + ctx->sCounters.tree.u64CyclesEvalPawnsMiss += u64Elapsed; + ctx->sCounters.tree.u64CountEvalPawnsMiss++; + } + } + } +#else + pHash = _EvalPawns(ctx, &fDeferred); +#endif ASSERT(NULL != pHash); ASSERT(IS_VALID_FLAG(fDeferred)); pos->iScore[WHITE] += pHash->iScore[WHITE]; -- cgit v1.3