summaryrefslogtreecommitdiff
path: root/src/chess.h
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-16 08:02:21 -0700
committerScott Gasch <[email protected]>2026-09-16 08:02:21 -0700
commit966a0ef4da9cf3911eb3da0533179199808717b4 (patch)
treee09363b574215ef0d8996155a5ab24314db2dd69 /src/chess.h
parentb91c8453f3fe21c8ccd3568923d8e39141c9c945 (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-xsrc/chess.h188
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); \