summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-04 09:22:46 -0700
committerScott Gasch <[email protected]>2026-09-04 09:22:46 -0700
commitbe420bb8d1d5d16a4e24ab6fd706a5ae898eaa85 (patch)
tree1dff11ecfec08780abfb7e95715ed0f14df1aac5
parent6c045be8a37a8eae1ea6aed250944e22af2335a1 (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
-rwxr-xr-xsrc/bitboard.c2
-rwxr-xr-xsrc/board.c10
-rw-r--r--src/board_representation/MIGRATION.md219
-rwxr-xr-xsrc/chess.h50
-rwxr-xr-xsrc/data.c73
-rwxr-xr-xsrc/fen.c1
-rwxr-xr-xsrc/main.c1
-rwxr-xr-xsrc/move.c14
-rwxr-xr-xsrc/see.c263
-rw-r--r--src/testsee.c136
-rw-r--r--src/testsup.c2
11 files changed, 712 insertions, 59 deletions
diff --git a/src/bitboard.c b/src/bitboard.c
index 9e9df48..ad45ea6 100755
--- a/src/bitboard.c
+++ b/src/bitboard.c
@@ -322,7 +322,7 @@ Return value:
return(0);
}
-COOR
+COOR
CoorFromBitBoardRank8ToRank1(BITBOARD *pbb)
/**
diff --git a/src/board.c b/src/board.c
index 0c5f4df..0b195be 100755
--- a/src/board.c
+++ b/src/board.c
@@ -169,6 +169,7 @@ Return value:
"Extra pieces on board that are not accounted for",
"Fifty move counter is too high",
"bbPieces bitboard doesn't match piece list",
+ "bbPawns bitboard doesn't match pawn list",
};
ULONG u, v;
COOR c;
@@ -177,6 +178,7 @@ Return value:
ULONG uNonPawnMaterial[2] = {0, 0};
ULONG uNonPawnCount[2][7];
BITBOARD bbPieces[2][8];
+ BITBOARD bbPawns[2];
ULONG uSigmaNonPawnCount[2] = {0, 0};
ULONG uWhiteSqBishopCount[2] = {0, 0};
UINT64 u64Computed;
@@ -186,6 +188,7 @@ Return value:
memset(uNonPawnCount, 0, sizeof(uNonPawnCount));
memset(bbPieces, 0, sizeof(bbPieces));
+ memset(bbPawns, 0, sizeof(bbPawns));
u64Computed = ComputeSig(pos);
if (pos->u64NonPawnSig != u64Computed)
{
@@ -268,6 +271,7 @@ Return value:
}
uPawnMaterial[u] += VALUE_PAWN;
uPawnCount[u]++;
+ bbPawns[u] |= COOR_TO_BB(c);
}
}
@@ -364,6 +368,12 @@ Return value:
}
}
+ if (pos->bbPawns[u] != bbPawns[u])
+ {
+ uReason = 22;
+ goto end;
+ }
+
//
// Note: the 0th spot in the array is the sum of all non pawns
//
diff --git a/src/board_representation/MIGRATION.md b/src/board_representation/MIGRATION.md
index 226e37e..f13aa9b 100644
--- a/src/board_representation/MIGRATION.md
+++ b/src/board_representation/MIGRATION.md
@@ -1,8 +1,13 @@
# Migration plan: bitboard-backed `GetAttacks` and `CountKingSafetyDefects`
-**Status: section 1 (and its correctness gate, section 2) implemented,
-verified, uncommitted on disk. Sections 3-8 not started.** See the
-per-section status notes below for specifics.
+**Status: sections 1-2 (bbPieces) and section 3's `GetAttacks` half
+(bbPieces/bbPawns-backed `_WhoAttacksSquareBB`/`_GetAttacksBB`, not yet
+wired into the `GetAttacks` macro) implemented and verified, ready to
+commit. `CountKingSafetyDefects`'s half of section 3 deliberately not
+started -- see its own status note below for why it needs a different
+design, not just a second consumer of the same primitive. Sections 4
+(partially done, folded into section 3's own testing)-8 otherwise not
+started.** See the per-section status notes below for specifics.
Context: a prior pass of this session added occupancy-bitboard-driven
mobility/attack-presence bitboards (`bbPawnAttacks`, `bbMinorAttacks`,
@@ -201,52 +206,180 @@ behavioral risk, exactly as planned. **Not yet committed** -- sitting on
disk, verified, ready to commit whenever section 1 is considered a good
checkpoint.
-## 3. New `_WhoAttacksSquareBB` primitive, consumed by two PoCs -- NOT STARTED
+## 3. New `_WhoAttacksSquareBB` primitive -- `GetAttacks` half DONE, `CountKingSafetyDefects` half not started
-Everything below this point is still just the plan -- no code written yet.
+### `GetAttacks` half -- DONE, verified correct, verified faster than asm
-Build one shared bitboard primitive rather than two independent
-implementations, since `GetAttacks` and `CountKingSafetyDefects` are the
-same query against different target squares:
+Implemented in `see.c`: `_WhoAttacksSquareBB(pos, cSquare, uSide,
+bbOccupied)` returns a bitboard of every uSide knight/bishop/rook/
+queen/king attacking `cSquare` (pawns handled separately, see below),
+and `_GetAttacksBB` wraps it into a `SEE_LIST`-populating PoC with the
+same signature/semantics as `SlowGetAttacks`/`GetAttacks`. Not yet
+wired into the `GetAttacks` macro (section 6's toggle) -- still a
+side-by-side PoC, called only from the test/bench harness described
+below.
-- Pawns: existing 2-square delta check, unchanged (already O(1), nothing
- to improve).
-- Knights/king: `g_KnightAttacksBB[cSquare] & bbPieces[side][KNIGHT]`
- (and the equivalent king delta check) -- O(1), reusing
- `g_KnightAttacksBB` already built and verified this session.
-- Sliders (bishop/rook/queen): reuse `g_RookRayToEdge`/`g_BishopRayToEdge`
- + `_FastFirstBit`/`_FastLastBit`, walking outward from the target
- square along each ray to the nearest blocker, testing it against
- `bbPieces[side][ROOK]`/`[BISHOP]`/`[QUEEN]` (queens checked on both ray
- sets). Needs `bbOccupied` (both colors, all pieces) to find the
- blocker -- construct as `bbPieces[WHITE][*] | bbPieces[BLACK][*] |
- pHash->bbPawnLocations[*] | king bits`, now cheap (all pre-maintained)
- rather than a `cNonPawns` walk.
+Diverged from the original plan in a few ways, all discovered by
+measuring rather than guessing:
-Two call-site wrappers, each a PoC alongside the real thing (same pattern
-as this session's earlier `_EvalRookOccupancyBB` PoC work), gated so old
-and new are both callable side by side:
+- **`bbOccupied` construction dropped the `pHash->bbPawnLocations`
+ idea.** That field lives behind `PawnHashLookup(SEARCHER_THREAD_CONTEXT
+ *)`, but `GetAttacks`'s real call sites (`generate.c` x5, `see.c`,
+ `searchsup.c`) only ever have a `POSITION *`, matching `_GetAttacksBB`'s
+ signature -- threading a `ctx` through everywhere just for this
+ wasn't worth it. Added `POSITION.bbPawns[2]` instead: plain,
+ incrementally-maintained state (identical pattern to `bbPieces`,
+ same 6 `move.c` sites plus `fen.c`'s from-scratch population),
+ independent of the pawn hash. This turned out to be the single
+ biggest win in the whole section -- see benchmarks below.
+- **A precomputed `g_PawnAttackOriginBB[2][128]` table** (data.c,
+ `InitializePawnAttackOriginTable`, called from `main.c`'s startup
+ sequence) replaced the original plan's "existing 2-square delta
+ check, unchanged" for pawns. That plan text assumed the delta check
+ was already optimal since it's O(1) either way -- true
+ asymptotically, but the *fixed* per-call instruction count still
+ mattered: `g_PawnAttackOriginBB[uSide][cSquare] & pos->bbPawns[uSide]`
+ is one table lookup + one AND, entirely in bit-space (no
+ `cSquare + delta` arithmetic, no `IS_ON_BOARD` branch, no mailbox
+ load), vs. the original's add/mask/load/compare done twice. Measured
+ a further ~2-4% win from this on top of the `bbPawns` change, small
+ but real and consistent across all three density buckets tested.
+- **Per-direction and per-side-group early-outs**, not just "does
+ uSide have any rook/queen at all": `bbRookSliders & g_RookRayAll[
+ cSquare]` (a new startup-built table, all 4 `g_RookRayToEdge`
+ directions pre-ORed per square) gates entry to the direction loop at
+ all; inside it, each direction is skipped via `bbRay & bbRookSliders`
+ before touching `bbOccupied` or calling `FastFirstBit`/`FastLastBit`
+ (same idea as bishop). This is the bitboard equivalent of what
+ `CHECK_VECTOR` does per-piece in the mailbox version.
+- **`FastFirstBit`/`FastLastBit`**: `static inline` compiler-builtin
+ (`__builtin_ctzll`/`__builtin_clzll`) wrappers, moved to `chess.h`
+ (not `bitboard.c` -- a non-`static` C99 `inline` split across a .c/.h
+ pair is a link-time trap with no out-of-line instantiation anywhere;
+ caught this by testing the link, not by inspection). Same bsf/bsr
+ instruction the asm `FirstBit`/`LastBit` already use (confirmed by
+ reading the compiled asm and `x64.asm`'s `FirstBit` directly) --
+ the entire benefit is skipping `CDECL` call/return overhead by
+ inlining, not a smarter bit-trick.
+- **Blocker isolation stays in bit-space**: the slider loop isolates
+ the nearest blocker as a bitboard bit directly (`bb & -bb` for the
+ positive-direction/lowest-bit case, no `FastFirstBit` call needed at
+ all; `1ULL << (FastLastBit(bb)-1)` for the negative/highest-bit
+ case) and tests/ORs it directly against other bitboards, instead of
+ round-tripping through `BIT_NUMBER_TO_COOR`/`COOR_TO_BB` to do the
+ same test. `COOR_TO_BB`/`BIT_NUMBER_TO_COOR` are themselves cheap
+ (~1 cycle, no memory access -- confirmed via `testbitboard.c`'s
+ existing benchmark), so this wasn't about macro cost, just removing
+ otherwise-pointless conversions between two representations that
+ never needed to leave bit-space in the first place.
+- **One deliberately-reverted attempt, kept as a comment**: replacing
+ the 4-direction "check ray, skip if empty" loop with code that
+ derived the *specific* needed direction(s) directly from the aligned
+ slider bits (via `FastFirstBit` + rank/file-nibble comparison, no
+ table lookup) measured *slower* (~1.07-1.09x asm vs. ~0.96-1.00x for
+ the simple loop) -- the bit-scan-to-avoid-a-cheap-branch traded a
+ cheap AND+continue for a costlier extraction+branch. Reverted; the
+ comment in `see.c` explains why, so it isn't rediscovered and
+ retried the same way.
-- **`_GetAttacksBB`** (`see.c`): calls the primitive against the move's
- destination square, populates a `SEE_LIST` exactly like `GetAttacks`
- does today. No pin-detection, no en-passant handling -- matching
- `GetAttacks`'s exact (deliberately approximate) semantics. Not
- introducing new behavior, just recomputing the same answer differently.
-- **`_CountKingSafetyDefectsBB`** (`eval.c`): calls the primitive against
- the king's square, feeding the same `CHECK_VECTOR`-derived defect-count
- logic `CountKingSafetyDefects` already has (`uPiecesPointingAtKing`
- bookkeeping included) -- only the "which enemy pieces bear on this
- square" step changes, the counting/scoring logic downstream of it
- doesn't.
+**Benchmark** (new `TestGetAttacks` addition in `testsee.c`, interleaved
+`GetAttacks`(asm)/`SlowGetAttacks`/`_GetAttacksBB`, 200k calls each
+across opening/middlegame/endgame positions, `SystemReadTimeStampCounter`-based,
+same methodology as `testbitboard.c`'s existing benchmark):
-`CountKingSafetyDefects` doesn't build a `SEE_LIST` at all (it just
-counts and classifies), so it isn't a second consumer of `_GetAttacksBB`
-directly -- it's a second consumer of the lower-level per-piece-type
-bitboard scan the two share. Structure the primitive so it can return
-either "the raw attacker bitboard per piece type" (what
-`CountKingSafetyDefects` wants, to test `CHECK_VECTOR`-equivalent
-conditions itself) or "a populated `SEE_LIST`" (what `GetAttacks` wants),
-rather than forcing one call shape on both.
+| stage | vs. asm `GetAttacks`, final |
+|---|---|
+| opening | **0.53x** (i.e. ~1.9x faster) |
+| middlegame | **0.55x** |
+| endgame | **0.89x** |
+
+The `bbPawns`/`_BuildOccupiedBB` change alone (before the pawn-origin
+table) accounted for most of the opening/middlegame jump -- from
+~0.96-1.00x to ~0.55-0.57x -- since it replaced an up-to-16-iteration
+loop over `cPawns[2][8]` with two ORs, and opening/middlegame positions
+are exactly where that loop was longest. Endgame barely moved (fewer
+pawns = the old loop was already cheap there), which is the expected
+shape for that change and a useful cross-check that the win is coming
+from where the theory says it should.
+
+**Correctness**: `SeeListsAreEqual` (testsee.c) made order-independent
+first (sorts a scratch copy of each list by `(cLoc, pPiece)` before
+comparing -- `SEE()` sorts/heaps the list immediately after `GetAttacks`
+returns, so order was never semantically significant, just untested as
+such before). `TestGetAttacks` extended to run `_GetAttacksBB` as a
+third comparison against the same 20,000-random-position x every-square
+x both-colors sweep already exercising `SlowGetAttacks`/asm `GetAttacks`
+-- clean, `gmake TEST=1`. This is section 4's items 1-2, done here
+rather than deferred, since it was the only way to get real confidence
+in the optimization work above rather than trusting cycle counts on a
+possibly-wrong implementation.
+
+**Two real bugs found and fixed while getting the correctness sweep to
+actually run (not just to pass)**, both pre-existing, both unrelated to
+`GetAttacks` itself:
+
+1. **`GenerateRandomLegalPosition` (testsup.c)** -- used by
+ `TestGetAttacks`'s 20,000-position sweep -- builds positions via
+ direct field writes (`cNonPawns[]`, `cPawns[]`, etc.), bypassing
+ `PlacePiece`/`SlidePawn` entirely, so it never maintained `bbPieces`
+ (already true as of section 1, latent since that commit) or the new
+ `bbPawns`. Its own legality gate, `VerifyPositionConsistency(pos,
+ TRUE)`, checks `bbPieces`/`bbPawns` against the piece list and
+ silently returns `FALSE` (no panic -- `fContinue=TRUE`) for any
+ position with a non-pawn officer or any pawn on the board, so the
+ generator's outer retry loop would almost always discard the
+ position and retry -- not a deterministic hang, but a large,
+ variable slowdown (sometimes fast by luck, sometimes ran for
+ minutes) that looked exactly like a new infinite loop the first
+ time it was hit. Root-caused by instrumenting
+ `VerifyPositionConsistency`'s failure path directly (temporary
+ `fprintf` of `uReason`) rather than guessing -- confirmed reason 21
+ (`bbPieces` mismatch), not the new reason 22 (`bbPawns` mismatch).
+ Fixed by adding the same one-line-per-site `bbPieces`/`bbPawns`
+ maintenance `fen.c` already had, to `testsup.c`'s two hand-placement
+ sites.
+2. **A pre-existing, non-deterministic assertion** (`util.c:1093`,
+ inside `FinishPVTailFromHash`'s PV-tail formatting, part of the
+ original 2016 commit, untouched by this or any prior session's
+ diff) surfaced once during `precommit_check.sh`'s DEBUG smoke test
+ on an unlucky random ECM sample. Confirmed independently of this
+ session's work: replaying the exact saved failing sample
+ (`--cpus 4 --hash 64m`, matching `debug_smoke_test.sh`'s invocation)
+ against a stashed-clean `HEAD` reproduced the same crash on 1 of 3
+ runs, and passed clean on the other 2 -- an `MP=1` (4-thread) race
+ in PV printing, unrelated to `GetAttacks`/`bbPieces`/`bbPawns`.
+ **Not fixed here** -- flagged for a future session, since fixing it
+ is unrelated scope to this migration and risks masking whether a
+ real regression exists if conflated with this work's own testing.
+
+### `CountKingSafetyDefects` half -- NOT STARTED, needs a different design than planned
+
+While implementing the `GetAttacks` half, re-reading the actual
+`CountKingSafetyDefects` body (`eval.c:2325`) revealed the original
+plan's premise for this half is wrong: the plan assumed both functions
+do "a mailbox ray-walk... to answer 'does this piece attack this
+square'", making them two consumers of one shared blocked-attack
+primitive. In fact `CountKingSafetyDefects` does **no ray-walk and no
+blocker/occlusion check at all** -- it's a `CHECK_VECTOR_WITH_INDEX`
+table lookup testing whether each enemy piece's *type* geometrically
+aligns with one of three squares near the king (`cKing-1, cKing,
+cKing+1`), regardless of what's in between. It's a deliberately
+approximate proximity/alignment heuristic, not a true attack query.
+
+Feeding it `_WhoAttacksSquareBB`'s real, blocker-aware result would
+silently make it *more accurate* than today -- a behavior change (and
+one that would need eval re-tuning + re-gating), not a value-identical
+reimplementation, which breaks section 4's "identical behavior" testing
+premise for this half specifically. Decided direction (not yet
+implemented): two separate functions instead of one shared code path --
+`_WhoAttacksSquareBB` stays `GetAttacks`-only; a `_CountKingSafetyDefectsBB`
+would keep the existing unblocked `CHECK_VECTOR` logic bit-for-bit,
+just iterate `bbPieces[xside][*]` bits instead of walking `cNonPawns[
+xside][]` to get the same enemy-piece-square list -- same "share
+`bbPieces` as the substrate, not a literal ray-walk" relationship as
+`GetAttacks`, just without a shared primitive function. Not started;
+next session should implement this shape directly rather than
+revisiting the original one-primitive premise.
## 4. Correctness verification
diff --git a/src/chess.h b/src/chess.h
index 1f874b9..5c5bbf2 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -658,6 +658,17 @@ typedef struct _POSITION
// (cNonPawns[color][0]), a bitboard adds nothing.
BITBOARD bbPieces[2][8];
+ // Per-color pawn location bitboard -- same incremental-maintenance
+ // idea as bbPieces above, but for pawns (which bbPieces
+ // deliberately excludes). Exists so _BuildOccupiedBB (see.c) can
+ // build full-board occupancy via two ORs instead of looping
+ // cPawns[2][8] (up to 16 iterations) on every call -- see
+ // board_representation/MIGRATION.md section 3. Distinct from
+ // pHash->bbPawnLocations[2] (pawn-hash-keyed, tied to pawn-eval
+ // caching); this one is plain POSITION state, reachable without a
+ // SEARCHER_THREAD_CONTEXT.
+ BITBOARD bbPawns[2];
+
ULONG uWhiteSqBishopCount[2]; // num bishops on white squares
SCORE iMaterialBalance[2]; // material balance
@@ -2027,12 +2038,15 @@ extern BITBOARD BBADJACENT_RANKS[9];
// board_representation/MIGRATION.md) the planned bbPieces-backed
// GetAttacks/CountKingSafetyDefects primitive.
extern BITBOARD g_RookRayToEdge[4][128];
+extern BITBOARD g_RookRayAll[128];
extern const int g_RookRayDeltas[4];
extern const FLAG g_RookRayPositiveDir[4];
extern BITBOARD g_BishopRayToEdge[4][128];
+extern BITBOARD g_BishopRayAll[128];
extern const int g_BishopRayDeltas[4];
extern const FLAG g_BishopRayPositiveDir[4];
extern BITBOARD g_KnightAttacksBB[128];
+extern BITBOARD g_PawnAttackOriginBB[2][128];
void
InitializeWhiteSquaresTable(void);
@@ -2055,6 +2069,9 @@ InitializeBishopRayTables(void);
void
InitializeKnightAttackTables(void);
+void
+InitializePawnAttackOriginTable(void);
+
#ifdef DEBUG
ULONG CheckVectorWithIndex(int i, ULONG uColor);
#define CHECK_VECTOR_WITH_INDEX(i, color) \
@@ -2797,6 +2814,31 @@ SlowFirstBit(BITBOARD bb);
ULONG CDECL
SlowLastBit(BITBOARD bb);
+// Compiler-builtin (__builtin_ctzll/__builtin_clzll) bsf/bsr -- same
+// 1-based/0-for-empty contract as FirstBit/LastBit below, and the
+// exact same bsf/bsr instruction the asm FirstBit/LastBit use (this
+// build passes no -mbmi, so __builtin_ctzll still lowers to bsf, not
+// tzcnt) -- but static inline, so a call site pays for the
+// instruction itself and nothing else, no CDECL call/ret/arg-marshal
+// overhead. That overhead is exactly what makes it worth having a
+// second copy instead of just calling FirstBit/LastBit everywhere:
+// worthwhile in a per-move-generated, per-search-node hot path,
+// pointless as a blanket replacement elsewhere. static (not extern)
+// deliberately -- a plain non-static C99 "inline" definition with no
+// out-of-line instantiation anywhere is a link-time trap, not just a
+// style choice.
+static ULONG INLINE
+FastFirstBit(IN BITBOARD bb)
+{
+ return bb ? ((ULONG)__builtin_ctzll(bb) + 1) : 0;
+}
+
+static ULONG INLINE
+FastLastBit(IN BITBOARD bb)
+{
+ return bb ? (ULONG)(64 - __builtin_clzll(bb)) : 0;
+}
+
#ifdef CROUTINES
#define CountBits SlowCountBits
#define FirstBit SlowFirstBit
@@ -2856,6 +2898,14 @@ SlowGetAttacks(SEE_LIST *pList,
#define GetAttacks SlowGetAttacks
#endif
+// board_representation/MIGRATION.md section 3: bbPieces-backed
+// GetAttacks PoC -- not wired into the GetAttacks macro above yet.
+void CDECL
+_GetAttacksBB(SEE_LIST *pList,
+ POSITION *pos,
+ COOR cSquare,
+ ULONG uSide);
+
#ifdef _X86_
//
// Note: this is most of the stuff that x86.asm assumes about the
diff --git a/src/data.c b/src/data.c
index 1ebbeff..217f1e8 100755
--- a/src/data.c
+++ b/src/data.c
@@ -578,6 +578,15 @@ BITBOARD g_RookRayToEdge[4][128];
const int g_RookRayDeltas[4] = { 16, -16, 1, -1 }; // N, S, E, W (0x88)
const FLAG g_RookRayPositiveDir[4] = { TRUE, FALSE, TRUE, FALSE };
+// Per-square OR of all 4 g_RookRayToEdge directions -- "every square a
+// rook on c could reach on an empty board, regardless of direction."
+// One lookup (+ AND against a slider bitboard) to answer "is uSide's
+// rook/queen bitboard aligned with c *at all*", vs. 4 separate
+// g_RookRayToEdge lookups to discover the same "no" -- see
+// _WhoAttacksSquareBB (see.c) for the consumer and
+// board_representation/MIGRATION.md section 3 for the writeup.
+BITBOARD g_RookRayAll[128];
+
void
InitializeRookRayTables(void)
/**
@@ -600,6 +609,7 @@ Return value:
COOR c, cSquare;
memset(g_RookRayToEdge, 0, sizeof(g_RookRayToEdge));
+ memset(g_RookRayAll, 0, sizeof(g_RookRayAll));
for (uRank = 0; uRank < 8; uRank++)
{
for (uFile = 0; uFile < 8; uFile++)
@@ -612,6 +622,7 @@ Return value:
cSquare += g_RookRayDeltas[uDir])
{
g_RookRayToEdge[uDir][c] |= COOR_TO_BB(cSquare);
+ g_RookRayAll[c] |= COOR_TO_BB(cSquare);
}
}
}
@@ -625,6 +636,9 @@ BITBOARD g_BishopRayToEdge[4][128];
const int g_BishopRayDeltas[4] = { 17, -17, 15, -15 }; // NE, SW, NW, SE (0x88)
const FLAG g_BishopRayPositiveDir[4] = { TRUE, FALSE, TRUE, FALSE };
+// g_RookRayAll's counterpart for the bishop's 4 diagonal directions.
+BITBOARD g_BishopRayAll[128];
+
void
InitializeBishopRayTables(void)
/**
@@ -647,6 +661,7 @@ Return value:
COOR c, cSquare;
memset(g_BishopRayToEdge, 0, sizeof(g_BishopRayToEdge));
+ memset(g_BishopRayAll, 0, sizeof(g_BishopRayAll));
for (uRank = 0; uRank < 8; uRank++)
{
for (uFile = 0; uFile < 8; uFile++)
@@ -659,6 +674,7 @@ Return value:
cSquare += g_BishopRayDeltas[uDir])
{
g_BishopRayToEdge[uDir][c] |= COOR_TO_BB(cSquare);
+ g_BishopRayAll[c] |= COOR_TO_BB(cSquare);
}
}
}
@@ -720,3 +736,60 @@ Return value:
}
}
}
+
+//
+// Per-square, per-side "the (up to 2) squares a pawn of this side
+// would need to stand on to attack c" bitboard -- e.g.
+// g_PawnAttackOriginBB[WHITE][c] is c's two SE/SW neighbors (a white
+// pawn attacks diagonally forward, so it must stand behind-and-beside
+// c to hit it). Same idea as g_KnightAttacksBB: a single lookup+AND
+// against bbPawns[side] answers "does uSide have a pawn attacking c"
+// entirely in bit-space, no COOR arithmetic/IS_ON_BOARD check at
+// runtime -- see _GetAttacksBB (see.c) for the consumer.
+//
+BITBOARD g_PawnAttackOriginBB[2][128];
+
+void
+InitializePawnAttackOriginTable(void)
+/**
+
+Routine description:
+
+ One-time startup init for g_PawnAttackOriginBB -- see its comment.
+
+Parameters:
+
+ void
+
+Return value:
+
+ void
+
+**/
+{
+ static const int iSeeDelta[2] = { -17, +15 }; // BLACK, WHITE
+ ULONG uRank, uFile, uSide;
+ COOR c, cOrigin;
+
+ memset(g_PawnAttackOriginBB, 0, sizeof(g_PawnAttackOriginBB));
+ for (uRank = 0; uRank < 8; uRank++)
+ {
+ for (uFile = 0; uFile < 8; uFile++)
+ {
+ c = (uRank << 4) | uFile;
+ for (uSide = 0; uSide < 2; uSide++)
+ {
+ cOrigin = c + iSeeDelta[uSide];
+ if (IS_ON_BOARD(cOrigin))
+ {
+ g_PawnAttackOriginBB[uSide][c] |= COOR_TO_BB(cOrigin);
+ }
+ cOrigin += 2;
+ if (IS_ON_BOARD(cOrigin))
+ {
+ g_PawnAttackOriginBB[uSide][c] |= COOR_TO_BB(cOrigin);
+ }
+ }
+ }
+ }
+}
diff --git a/src/fen.c b/src/fen.c
index 8a2dcc9..2baa05d 100755
--- a/src/fen.c
+++ b/src/fen.c
@@ -300,6 +300,7 @@ Return value:
// location to the list and point from the pawn to the list.
//
pos->cPawns[uColor][uPieceCounters[uPieceIndex]] = cSquare;
+ pos->bbPawns[uColor] |= COOR_TO_BB(cSquare);
pos->rgSquare[cSquare].uIndex = uPieceCounters[uPieceIndex];
pos->rgSquare[cSquare].pPiece = p;
pos->uPawnMaterial[uColor] += VALUE_PAWN;
diff --git a/src/main.c b/src/main.c
index 3c94fcc..9abc407 100755
--- a/src/main.c
+++ b/src/main.c
@@ -464,6 +464,7 @@ Return value:
InitializeRookRayTables();
InitializeBishopRayTables();
InitializeKnightAttackTables();
+ InitializePawnAttackOriginTable();
InitializeOpeningBook();
InitializeDynamicMoveOrdering();
InitLMRTable();
diff --git a/src/move.c b/src/move.c
index 1ed1485..44e7073 100755
--- a/src/move.c
+++ b/src/move.c
@@ -121,6 +121,8 @@ Return value:
ASSERT(IS_VALID_COLOR(c));
ASSERT(pos->cPawns[c][uIndex] == cFrom);
pos->cPawns[c][uIndex] = cTo;
+ pos->bbPawns[c] &= ~COOR_TO_BB(cFrom);
+ pos->bbPawns[c] |= COOR_TO_BB(cTo);
pos->u64PawnSig ^= g_u64PawnSigSeeds[cFrom][c];
pos->u64PawnSig ^= g_u64PawnSigSeeds[cTo][c];
pos->rgSquare[cTo].pPiece = p;
@@ -224,6 +226,8 @@ Return value:
ASSERT(IS_VALID_COLOR(c));
ASSERT(pos->cPawns[c][uIndex] == cFrom);
pos->cPawns[c][uIndex] = cTo;
+ pos->bbPawns[c] &= ~COOR_TO_BB(cFrom);
+ pos->bbPawns[c] |= COOR_TO_BB(cTo);
pos->rgSquare[cTo].pPiece = p;
pos->rgSquare[cTo].uIndex = uIndex;
#ifdef DEBUG
@@ -293,10 +297,11 @@ Return value:
ASSERT(pos->uPawnMaterial[color] <= (7 * VALUE_PAWN));
pos->u64PawnSig ^= g_u64PawnSigSeeds[cSquare][color];
+ pos->bbPawns[color] &= ~COOR_TO_BB(cSquare);
//
// Remove this pawn from the pawn list.
- //
+ //
pos->uPawnCount[color]--;
ASSERT(pos->uPawnCount[color] < 8);
uLastIndex = pos->uPawnCount[color];
@@ -426,10 +431,11 @@ Return value:
pos->uPawnMaterial[color] -= pv;
ASSERT(pos->uPawnMaterial[color] <= (7 * VALUE_PAWN));
-
+ pos->bbPawns[color] &= ~COOR_TO_BB(cSquare);
+
//
// Remove this pawn from the pawn list.
- //
+ //
pos->uPawnCount[color]--;
ASSERT(pos->uPawnCount[color] < 8);
uLastIndex = pos->uPawnCount[color]; // optimized...
@@ -542,6 +548,7 @@ Return value:
pos->uPawnCount[color]++;
ASSERT(pos->uPawnCount[color] <= 8);
pos->cPawns[color][uIndex] = cSquare;
+ pos->bbPawns[color] |= COOR_TO_BB(cSquare);
pos->u64PawnSig ^= g_u64PawnSigSeeds[cSquare][color];
}
else
@@ -634,6 +641,7 @@ Return value:
pos->uPawnCount[color]++;
ASSERT(pos->uPawnCount[color] <= 8);
pos->cPawns[color][uIndex] = cSquare;
+ pos->bbPawns[color] |= COOR_TO_BB(cSquare);
}
else
{
diff --git a/src/see.c b/src/see.c
index 292cbf1..4b534bb 100755
--- a/src/see.c
+++ b/src/see.c
@@ -157,6 +157,267 @@ Return value:
}
}
+
+//
+// board_representation/MIGRATION.md section 3: bbPieces-backed
+// "who attacks square X" primitive, and a GetAttacks PoC built on
+// it. Not wired into the GetAttacks macro yet -- see MIGRATION.md
+// section 6 for the eventual toggle. Uses chess.h's FastFirstBit/
+// FastLastBit (static inline bsf/bsr wrappers) rather than the real
+// out-of-line FirstBit/LastBit -- worth avoiding call overhead in a
+// per-move-generated, per-node hot path like this one.
+//
+static BITBOARD
+_BuildOccupiedBB(IN POSITION *pos)
+/**
+
+Routine description:
+
+ Full-board occupancy (both colors, every piece including pawns
+ and kings), built from the incrementally-maintained bbPieces[2][8]
+ and bbPawns[2] fields plus the king mailbox array
+ (cNonPawns[.][0], a single square per side -- a bitboard for that
+ adds nothing). All O(1) ORs now that bbPawns exists; this used to
+ loop cPawns[2][8] (up to 16 iterations) to build the pawn portion,
+ which ran on every single call regardless of how few pawns were
+ actually relevant.
+
+Parameters:
+
+ POSITION *pos
+
+Return value:
+
+ BITBOARD
+
+**/
+{
+ return (pos->bbPieces[WHITE][KNIGHT] | pos->bbPieces[WHITE][BISHOP] |
+ pos->bbPieces[WHITE][ROOK] | pos->bbPieces[WHITE][QUEEN] |
+ pos->bbPieces[BLACK][KNIGHT] | pos->bbPieces[BLACK][BISHOP] |
+ pos->bbPieces[BLACK][ROOK] | pos->bbPieces[BLACK][QUEEN] |
+ pos->bbPawns[WHITE] | pos->bbPawns[BLACK] |
+ COOR_TO_BB(pos->cNonPawns[WHITE][0]) |
+ COOR_TO_BB(pos->cNonPawns[BLACK][0]));
+}
+
+static 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, which handles
+ them the same 2-square-delta way SlowGetAttacks always has (already
+ O(1), nothing to improve).
+
+ Knights and the king are pure O(1) table/delta lookups (no
+ blocking possible). Sliders walk outward from cSquare along each
+ of the 4 rook/4 bishop directions to the *nearest* blocker
+ (g_RookRayToEdge/g_BishopRayToEdge ANDed with bbOccupied, reduced
+ via FastFirstBit/FastLastBit), and test only that nearest
+ blocker for membership in uSide's rook/bishop/queen bitboard --
+ anything beyond the first blocker on a ray cannot be attacking
+ cSquare regardless of its type, so only one square per direction
+ is ever classified.
+
+ Each 4-direction ray-walk is skipped entirely (bbRookSliders/
+ bbBishopSliders both zero) when uSide has no piece that could
+ possibly be found by it -- cheap up front, and the case that
+ matters most: a benchmark comparing this function's original
+ unconditional version against the real (asm) GetAttacks showed a
+ consistent ~1.4x slowdown across opening/middlegame/endgame
+ positions, because the unconditional 8-ray walk pays a fixed cost
+ regardless of how few of uSide's pieces are actually sliders,
+ while the mailbox version's cost scales with uSide's live piece
+ count. This early-out targets exactly that mismatch -- see
+ board_representation/MIGRATION.md section 3 for the writeup.
+
+Parameters:
+
+ POSITION *pos,
+ COOR cSquare : target square
+ ULONG uSide : side whose attackers on cSquare we want
+ BITBOARD bbOccupied : full-board occupancy (see _BuildOccupiedBB)
+
+Return value:
+
+ BITBOARD
+
+**/
+{
+ BITBOARD bbAttackers;
+ BITBOARD bbRookSliders;
+ BITBOARD bbBishopSliders;
+ BITBOARD bbRay;
+ BITBOARD bbBlockers;
+ BITBOARD bbBlockerBit;
+ ULONG u;
+
+ bbAttackers = g_KnightAttacksBB[cSquare] & pos->bbPieces[uSide][KNIGHT];
+ if (DISTANCE(cSquare, pos->cNonPawns[uSide][0]) == 1)
+ {
+ bbAttackers |= COOR_TO_BB(pos->cNonPawns[uSide][0]);
+ }
+
+ // Measured slower: deriving the needed direction(s) directly from
+ // the aligned slider bits (via FastFirstBit + rank/file-nibble
+ // comparison) instead of the plain 4-direction loop below. The
+ // extra bit-scan and branching to *avoid* touching 2-3 empty
+ // directions cost more than just touching them via a cheap
+ // AND+continue -- reverted; keeping the note so this isn't
+ // rediscovered as "obviously better" and retried the same way.
+ //
+ // g_RookRayAll[cSquare] (all 4 directions' masks pre-ORed at
+ // startup) answers "is uSide's rook/queen bitboard aligned with
+ // cSquare in *any* rook direction at all" in one lookup+AND,
+ // before paying for even the first per-direction check -- pieces
+ // that aren't on any rook line from cSquare get rejected right
+ // here. For the direction(s) that remain possible, g_RookRayToEdge[
+ // u][cSquare] & bbRookSliders is the bitboard equivalent of what
+ // CHECK_VECTOR does per-piece in the mailbox version -- "does
+ // uSide have a rook/queen on *this* ray specifically."
+ bbRookSliders = pos->bbPieces[uSide][ROOK] | pos->bbPieces[uSide][QUEEN];
+ if (bbRookSliders & g_RookRayAll[cSquare])
+ {
+ for (u = 0; u < 4; u++)
+ {
+ bbRay = g_RookRayToEdge[u][cSquare];
+ if (!(bbRay & bbRookSliders))
+ {
+ continue;
+ }
+ bbBlockers = bbRay & bbOccupied;
+ // Isolate the nearest blocker as a bitboard bit directly,
+ // skipping the bit-index/COOR round trip entirely --
+ // bbBlockers, bbRookSliders and bbAttackers are all
+ // already bitboards, so there's nothing COOR-space adds
+ // here. Lowest-bit isolation (positive-direction rays)
+ // doesn't even need FastFirstBit's ctz -- bb & -bb is O(1)
+ // with no bit-scan instruction at all; the negative
+ // direction still needs FastLastBit (no O(1) "isolate
+ // highest bit" trick exists without counting leading
+ // zeros first).
+ bbBlockerBit = g_RookRayPositiveDir[u] ?
+ (bbBlockers & (0ULL - bbBlockers)) :
+ (1ULL << (FastLastBit(bbBlockers) - 1));
+ bbAttackers |= (bbRookSliders & bbBlockerBit);
+ }
+ }
+
+ bbBishopSliders = pos->bbPieces[uSide][BISHOP] | pos->bbPieces[uSide][QUEEN];
+ if (bbBishopSliders & g_BishopRayAll[cSquare])
+ {
+ for (u = 0; u < 4; u++)
+ {
+ bbRay = g_BishopRayToEdge[u][cSquare];
+ if (!(bbRay & bbBishopSliders))
+ {
+ continue;
+ }
+ bbBlockers = bbRay & bbOccupied;
+ bbBlockerBit = g_BishopRayPositiveDir[u] ?
+ (bbBlockers & (0ULL - bbBlockers)) :
+ (1ULL << (FastLastBit(bbBlockers) - 1));
+ bbAttackers |= (bbBishopSliders & bbBlockerBit);
+ }
+ }
+
+ return bbAttackers;
+}
+
+void CDECL
+_GetAttacksBB(IN OUT SEE_LIST *pList,
+ IN POSITION *pos,
+ IN COOR cSquare,
+ IN ULONG uSide)
+/**
+
+Routine description:
+
+ PROOF OF CONCEPT -- not called from anywhere yet, and not a
+ replacement for GetAttacks/SlowGetAttacks until section 4/5/6 of
+ board_representation/MIGRATION.md (correctness sweep, benchmark,
+ toggle) are done. Reproduces SlowGetAttacks's exact semantics
+ (same deliberately-approximate no-pin/no-en-passant contract) via
+ _WhoAttacksSquareBB instead of the O(non-pawn-piece-count) mailbox
+ walk -- pawns handled identically to SlowGetAttacks (2-square
+ delta, unchanged, already O(1)).
+
+ Attacker order is not guaranteed to match SlowGetAttacks -- see()
+ sorts/heaps the list immediately after GetAttacks returns, so only
+ the *set* of attackers needs to match, not the sequence
+ (board_representation/MIGRATION.md section 4).
+
+Parameters:
+
+ SEE_LIST *pList : list to populate
+ POSITION *pos : the board
+ COOR cSquare : square in question
+ ULONG uSide : side we are looking for attacks from
+
+Return value:
+
+ void
+
+**/
+{
+ BITBOARD bbOccupied;
+ BITBOARD bbAttackers;
+ ULONG uBitIndex;
+ COOR c;
+ PIECE p;
+ static PIECE pPawn[2] = { BLACK_PAWN, WHITE_PAWN };
+
+#ifdef DEBUG
+ ASSERT(IS_ON_BOARD(cSquare));
+ ASSERT(IS_VALID_COLOR(uSide));
+ VerifyPositionConsistency(pos, FALSE);
+#endif
+ pList->uCount = 0;
+
+ //
+ // g_PawnAttackOriginBB[uSide][cSquare] (precomputed at startup --
+ // see data.c) is "the up to 2 squares a uSide pawn would need to
+ // stand on to attack cSquare," as a bitboard. One lookup + one AND
+ // against bbPawns[uSide] answers the whole question in bit-space --
+ // no COOR arithmetic (cSquare + iSeeDelta), no IS_ON_BOARD check,
+ // no mailbox load -- entirely replacing what iSeeDelta/pPawn[]
+ // used to do at runtime; only the (0-2) actual hits still need a
+ // COOR to populate the SEE_LIST.
+ {
+ BITBOARD bbPawnHits = g_PawnAttackOriginBB[uSide][cSquare] &
+ pos->bbPawns[uSide];
+ ULONG uPawnBit;
+
+ while (bbPawnHits)
+ {
+ uPawnBit = FastFirstBit(bbPawnHits) - 1;
+ bbPawnHits &= (bbPawnHits - 1);
+ ADD_ATTACKER(pPawn[uSide], BIT_NUMBER_TO_COOR(uPawnBit), VALUE_PAWN);
+ }
+ }
+
+ //
+ // Knights/bishops/rooks/queens/king, via the bitboard primitive.
+ //
+ bbOccupied = _BuildOccupiedBB(pos);
+ bbAttackers = _WhoAttacksSquareBB(pos, cSquare, uSide, bbOccupied);
+ while (bbAttackers)
+ {
+ uBitIndex = FastFirstBit(bbAttackers) - 1;
+ bbAttackers &= (bbAttackers - 1); // clear lowest set bit
+ c = BIT_NUMBER_TO_COOR(uBitIndex);
+ p = pos->rgSquare[c].pPiece;
+ ADD_ATTACKER(p, c, PIECE_VALUE(p));
+ }
+}
+
#ifdef SEE_HEAPS
//
// SEE_HEAPS works great in principle but makes MinLegalPiece
@@ -972,7 +1233,7 @@ Return value:
UtilPanic(TESTCASE_FAILURE,
NULL,
"See mismatch",
- rgiList[0],
+ rgiList[0],
iSign,
__FILE__, __LINE__);
}
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);
}
}
}
diff --git a/src/testsup.c b/src/testsup.c
index 43e39e9..3ed50e3 100644
--- a/src/testsup.c
+++ b/src/testsup.c
@@ -161,6 +161,7 @@ GenerateRandomLegalPosition(POSITION *pos)
pos->rgSquare[c].pPiece = p;
pos->uPawnCount[uColor]++;
pos->uPawnMaterial[uColor] += VALUE_PAWN;
+ pos->bbPawns[uColor] |= COOR_TO_BB(c);
break;
}
else if (!IS_KING(p) &&
@@ -173,6 +174,7 @@ GenerateRandomLegalPosition(POSITION *pos)
pos->rgSquare[c].pPiece = p;
pos->rgSquare[c].uIndex = uIndex;
pos->uNonPawnMaterial[uColor] += PIECE_VALUE(p);
+ pos->bbPieces[uColor][PIECE_TYPE(p)] |= COOR_TO_BB(c);
if (IS_BISHOP(p))
{
if (IS_WHITE_SQUARE_COOR(c))