diff options
| author | Scott Gasch <[email protected]> | 2026-09-16 08:02:21 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-16 08:02:21 -0700 |
| commit | 966a0ef4da9cf3911eb3da0533179199808717b4 (patch) | |
| tree | e09363b574215ef0d8996155a5ab24314db2dd69 /src/chess.h | |
| parent | b91c8453f3fe21c8ccd3568923d8e39141c9c945 (diff) | |
Retire cPawns/cNonPawns piece-location lists in favor of bbPawns/bbPieces/cKingSq, and hoist two redundant TotalNonPawnCount POPCNT calls out of per-node hot loops.
POSITION no longer stores cPawns[2][8], cNonPawns[2][16], uPawnCount[2],
uNonPawnCount[2][8], or uWhiteSqBishopCount[2] -- every reader now goes
through PawnMaterial/TotalNonPawnCount/WhiteSqBishopCount/KingPosition
(chess.h), computed from the already-incrementally-maintained bbPawns/
bbPieces/cKingSq bitboards via a single -mpopcnt POPCNT instead of a
stored counter. move.c's make/unmake loses the list-swap-on-removal
bookkeeping entirely. GNUmakefile now passes -mpopcnt so the popcount
builtins actually compile to one instruction instead of clang's ~15-op
SWAR fallback.
Measured a small but real nps regression from the naive swap (search.c's
QSearch move-filter and eval.c's _EvalKing were each recomputing the same
TotalNonPawnCount sum on every candidate move / twice per node); hoisted
both to a single call per node instead. Net result across a same-code
self-play match: ~1541.5 knps candidate vs ~1505.7 knps head_reference.
A third hoist (FindEnprisePiece out of SelectBestWithHistory) was tried
and reverted -- its cache isn't actually loop-invariant within a node
(populated from inside Eval(), which doesn't run before every selection
call), caught by a DEBUG-build ASSERT before it landed.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/chess.h')
| -rwxr-xr-x | src/chess.h | 188 |
1 files changed, 174 insertions, 14 deletions
diff --git a/src/chess.h b/src/chess.h index 38fdb3f..349ff1e 100755 --- a/src/chess.h +++ b/src/chess.h @@ -603,24 +603,47 @@ typedef struct _POSITION BITV bvCastleInfo; // who can castle how? COOR cEpSquare; // en-passant capture square - COOR cPawns[2][8]; // location of pawns on the board - ULONG uPawnMaterial[2]; // pawn material of each side - ULONG uPawnCount[2]; // number of pawns for each side + // Pawn material of each side -- every pawn is worth the same, so + // this (like uPawnCount/uNonPawnCount before it) is just + // FastCountBits(bbPawns[color]) * VALUE_PAWN (see PawnMaterial() + // below), not a stored field. - COOR cNonPawns[2][16]; // location of pieces on the board ULONG uNonPawnMaterial[2]; // piece material of each side - ULONG uNonPawnCount[2][8]; // number of non-pawns / type - // 0 and 1 are the sum, - // 2..6 are per PIECE_TYPE + // (counts: FastCountBits( + // bbPieces[2][KNIGHT/BISHOP/ + // ROOK/QUEEN]) per type; + // FastCountBits( + // NON_KING_PIECES_BB(pos, + // color)) + 1 for the total; + // always exactly 1 for KING, + // no backing bitboard exists) + + // King location, per color -- a plain scalar read/write. Used to + // carry the location the old cNonPawns[color][0] convention slot + // held (2026-09-16 board_representation/MIGRATION.md-style + // retirement of the cNonPawns[2][16] piece-location list: every + // reader outside move.c's own list-maintenance code turned out to + // be either this fixed king slot or a full-population walk, both + // servable directly by bbPieces[2][8] below via + // NON_KING_PIECES_BB/CoorFromBitBoardRank8ToRank1 -- see git log + // for the migration). NOT redundant with bbPieces[color][KING]: + // that slot is deliberately never populated (fen.c's IS_KING + // branch skips the bbPieces OR, board.c's + // VerifyPositionConsistency rebuild loop guards `v < KING` / + // `!IS_KING(p)` to match) -- and even if it were, every read would + // cost a CTZ + COOR conversion versus this field's one load. + // Maintained by SlidePiece (move.c) and fen.c's/testsup.c's setup + // code. + COOR cKingSq[2]; // Per-color, per-piece-type location bitboards -- "where are my // knights/bishops/rooks/queens" -- indexed by PIECE_TYPE exactly // like uNonPawnCount above (slots 0/1/PAWN/KING unused, same // convention). Maintained incrementally in move.c at every site - // that already updates cNonPawns[]/uNonPawnCount[] (see + // that already updates uNonPawnCount[] (see // board_representation/MIGRATION.md), not rebuilt -- O(1) per - // move. Pawns use bbPawns[2] below instead; king is a single - // square (cNonPawns[color][0]), a bitboard adds nothing. + // move. Pawns use bbPawns[2] below instead; king uses cKingSq[2] + // above, not this array (see cKingSq's own comment for why). BITBOARD bbPieces[2][8]; // Per-color pawn location bitboard -- same incremental-maintenance @@ -722,7 +745,6 @@ typedef struct _POSITION // bitboard for both colors. BITBOARD bbKingAttacks[2]; - ULONG uWhiteSqBishopCount[2]; // num bishops on white squares SCORE iMaterialBalance[2]; // material balance // temporary storage space for use in eval @@ -743,7 +765,6 @@ typedef struct _POSITION SCORE iScore[2]; SCORE iReducedMaterialDownScaler[2]; ULONG uMinorsAtHome[2]; - BITBOARD bb; } POSITION; @@ -3664,6 +3685,71 @@ FastLastBit(IN BITBOARD bb) return bb ? (ULONG)(64 - __builtin_clzll(bb)) : 0; } +// Same rationale as FastFirstBit/FastLastBit above: __builtin_popcountll +// compiles to a single POPCNT instruction (verified against this +// build's actual object code, not assumed -- GNUmakefile's -mpopcnt is +// required for this; without it clang silently falls back to a +// ~15-instruction SWAR sequence, still faster than the extern CDECL +// CountBits below -- x64.asm's real implementation is a Kernighan +// bit-clearing loop, O(popcount) iterations plus call overhead, +// measured at ~23 cycles/op by gmake TEST=1's own bitboard benchmark +// -- but not the ~1-cycle single instruction this function gets with +// -mpopcnt). Measured (rdtsc, -O3 -mpopcnt, inlined): statistically +// identical cost to a plain scalar field read for a single popcnt, and +// ~2.5 cycles for an OR-several-bitboards-then-popcnt compound count -- +// this is what retired uPawnCount[2]/uNonPawnCount[2][8]'s stored +// counts in favor of computing them from bbPawns/bbPieces on read. +static ULONG INLINE +FastCountBits(IN BITBOARD bb) +{ + ULONG u = (ULONG)__builtin_popcountll(bb); + ASSERT(u == SlowCountBits(bb)); + return u; +} + +// Fast, globally-available equivalents of CoorFromBitBoardRank8ToRank1/ +// CoorFromBitBoardRank1ToRank8 (bitboard.c) -- those are real +// out-of-line extern CDECL calls from every translation unit except +// eval.c (which has had its own private copy of exactly this pair +// since the cNonPawns/cPawns/uPawnCount/uNonPawnCount retirement work +// started adding multi-bit bitboard-scan loops elsewhere -- confirmed +// by disassembly that recogn.o/board.o/generate.o/movesup.o/probe.o/ +// testmove.o were still emitting real callq+PLT relocations to the +// slow extern version). Promoted here so every caller gets the fast +// path, not just eval.c. _FastCoorFromBitBoardRank1ToRank8 bakes in +// the same wrong-bit-clear fix as bitboard.c's real +// CoorFromBitBoardRank1ToRank8 (clears the bit it actually reported, +// via BBSQUARE, not unconditionally the lowest set bit). +static COOR INLINE +FastCoorFromBitBoardRank8ToRank1(IN OUT BITBOARD *pbb) +{ + COOR c = ILLEGAL_COOR; + ULONG uBitIndex; + + if (*pbb) + { + uBitIndex = (ULONG)__builtin_ctzll(*pbb); + c = BIT_NUMBER_TO_COOR(uBitIndex); + *pbb &= (*pbb - 1); + } + return c; +} + +static COOR INLINE +FastCoorFromBitBoardRank1ToRank8(IN OUT BITBOARD *pbb) +{ + COOR c = ILLEGAL_COOR; + ULONG uBitIndex; + + if (*pbb) + { + uBitIndex = (ULONG)(63 - __builtin_clzll(*pbb)); + c = BIT_NUMBER_TO_COOR(uBitIndex); + *pbb &= ~BBSQUARE[uBitIndex]; + } + return c; +} + #ifdef CROUTINES #define CountBits SlowCountBits #define FirstBit SlowFirstBit @@ -3676,6 +3762,82 @@ CoorFromBitBoardRank8ToRank1(BITBOARD *pbb); COOR CoorFromBitBoardRank1ToRank8(BITBOARD *pbb); +// King location -- reads the dedicated cKingSq[2] scalar (see its +// declaration in POSITION) instead of the old cNonPawns[color][0] +// convention. +static COOR INLINE +KingPosition(IN POSITION *pos, IN ULONG color) +{ + return pos->cKingSq[color]; +} + +// Combined "every non-pawn, non-king piece of this color" bitboard -- +// replaces a cNonPawns[color][1..count-1] list walk (slot 0 is always +// the king, see cKingSq above) with a scan over this bitboard. Bit +// order (rank8->rank1 via CoorFromBitBoardRank8ToRank1) differs from +// the old list's insertion order; fine for callers that just +// sum/count/search, not for callers that depended on insertion order +// specifically. +// +// bbOccupiedSide[color] & ~bbPawns[color] is every non-pawn piece of +// this color INCLUDING the king (bbOccupiedSide covers pawns, king, +// and every other piece type; subtracting bbPawns leaves king + +// knight/bishop/rook/queen) -- clearing the king's own bit on top of +// that is cheaper (one AND, one ANDNOT, one more ANDNOT) than the +// previous OR of all four per-type bitboards (three ORs), and doesn't +// need bbPieces[color][KING] to exist (it deliberately doesn't -- see +// cKingSq's comment). +#define NON_KING_PIECES_BB(pos, color) \ + ((pos)->bbOccupiedSide[(color)] & \ + ~(pos)->bbPawns[(color)] & \ + ~COOR_TO_BB((pos)->cKingSq[(color)])) + +// Non-destructive single-bit-to-COOR conversion -- unlike +// CoorFromBitBoardRank8ToRank1, doesn't clear the bit or take a +// pointer, so it's safe to call directly on a POSITION field. Callers +// must know the bitboard has exactly one bit set (e.g. bbPieces[color] +// [KNIGHT] in a position with only one knight of that color) -- +// behavior on an empty or multi-bit board is the same +// "first/lowest set bit, or ILLEGAL_COOR-shaped garbage if none" +// contract FastFirstBit already has. +static COOR INLINE +CoorOfSingleBit(IN BITBOARD bb) +{ + return BIT_NUMBER_TO_COOR(FastFirstBit(bb) - 1); +} + +// Total non-pawn piece count (including the king), replacing the old +// stored uNonPawnCount[color][0] convention slot -- see chess.h's +// uNonPawnMaterial comment for why this became a popcnt instead of a +// stored field. bbOccupiedSide[color] & ~bbPawns[color] already +// includes the king (unlike NON_KING_PIECES_BB, which excludes it) -- +// cheaper than FastCountBits(NON_KING_PIECES_BB(pos, color)) + 1, +// same reasoning as NON_KING_PIECES_BB's own comment. +static ULONG INLINE +TotalNonPawnCount(IN POSITION *pos, IN ULONG color) +{ + return FastCountBits(pos->bbOccupiedSide[color] & ~pos->bbPawns[color]); +} + +// Number of this color's bishops on white squares, replacing the old +// stored uWhiteSqBishopCount[2] field -- one AND against the constant +// BBWHITESQ bitboard (bitboard.c) plus a popcnt, same cost class as +// the other counts retired alongside it. +static ULONG INLINE +WhiteSqBishopCount(IN POSITION *pos, IN ULONG color) +{ + return FastCountBits(pos->bbPieces[color][BISHOP] & BBWHITESQ); +} + +// Pawn material of this color, replacing the old stored +// uPawnMaterial[2] field -- every pawn is worth VALUE_PAWN, so this is +// just a popcnt and a multiply. +static ULONG INLINE +PawnMaterial(IN POSITION *pos, IN ULONG color) +{ + return FastCountBits(pos->bbPawns[color]) * VALUE_PAWN; +} + // // x86.asm // @@ -3753,8 +3915,6 @@ _WhoAttacksSquareBB(POSITION *pos, ASSERT(OFFSET_OF(pPiece, SEE_THREESOME) == 0); \ ASSERT(OFFSET_OF(cLoc, SEE_THREESOME) == 4); \ ASSERT(OFFSET_OF(uVal, SEE_THREESOME) == 8); \ - ASSERT(OFFSET_OF(cNonPawns, POSITION) == 0x478); \ - ASSERT(OFFSET_OF(uNonPawnCount, POSITION) == 0x500); \ ASSERT(OFFSET_OF(rgSquare, POSITION) == 0); \ ASSERT(sizeof(SQUARE) == 8); \ ASSERT(sizeof(VECTOR_DELTA) == 4); \ |
