summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-04 23:54:51 -0700
committerScott Gasch <[email protected]>2026-09-04 23:54:51 -0700
commit5883f5a64f4f464b877b7637b8e13c25f7d208fc (patch)
tree97951281d86a02254be5682356fa5b29a5076a56 /src
parent92fc41226f784b251f41eab7e75c13075e980a54 (diff)
Add Eval() bitboard-migration plan, per-term EVAL_TIME cycle breakdown, drop redundant pawn-location bitboard
board_representation/EVAL.md: rewritten migration plan for a bitboard-backed Eval() (mobility ray-walks + bvAttacks replacement), plus a performance-philosophy section recording the profiling-first, cut-aggressively-except-mobility/safety-awareness approach agreed on this session, and findings on CountKingSafetyDefects' structural inability to share bvAttacks-derived state with _EvalKing. EVAL_TIME per-term instrumentation (chess.h/eval.c/root.c): breaks the existing whole-Eval() cycle counter down by pawns/knight/bishop/ rook/queen/king, the always-paid pre-lazy-exit segment, and the full-eval-only post-lazy segment, printed alongside the existing "Avg. cpu cycles in eval" line. Diagnostic only (EVAL_TIME-gated), no effect on the normal release profile. Drop PAWN_HASH_ENTRY's bbPawnLocations[2]: it duplicated POSITION's own incrementally-maintained bbPawns[2], rebuilt bit-by-bit on every pawn-hash miss for no reason. eval.c now reads pos->bbPawns[] directly; removed a stale per-iteration invariant assert in _EvalPawns that only made sense when the bitboard was being built bit-by-bit in that same loop. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj
Diffstat (limited to 'src')
-rw-r--r--src/board_representation/EVAL.md627
-rwxr-xr-xsrc/chess.h74
-rwxr-xr-xsrc/eval.c120
-rwxr-xr-xsrc/root.c52
4 files changed, 820 insertions, 53 deletions
diff --git a/src/board_representation/EVAL.md b/src/board_representation/EVAL.md
new file mode 100644
index 0000000..34fb9c3
--- /dev/null
+++ b/src/board_representation/EVAL.md
@@ -0,0 +1,627 @@
+# Migration plan: bitboard-backed `Eval()` (rewrite, 2026-09-04)
+
+**Status: planning only, superseding the earlier draft of this
+document in full.** That draft was written before the movegen
+bitboard work (`MOVEGEN_MIGRATION.md`) landed and assumed a slider
+attack-bitboard primitive ("`_RookAttacksBB(c, bbOccupied)`... already
+built") that did not actually exist yet at the time. It now does --
+this rewrite is based on having read `_EvalBishop`/`_EvalKnight`/
+`_EvalRook`/`_EvalQueen` in full (not by memory/analogy) and the
+landed `generate.c` bitboard infrastructure (real magic tables, not a
+sketch) side by side.
+
+## 0. Why this rewrite exists, and what's different now
+
+Two things changed since the first draft:
+
+1. **Real magic bitboards now exist and are directly reusable.**
+ `_RookAttacksBB(COOR c, BITBOARD bbOccupied)` / `_BishopAttacksBB(...)`
+ (`generate.c`) are `FORCEINLINE`, non-static, and already used from
+ three call sites outside their own file's move generation (the
+ Part B check-block-mask computation, and would be a fourth caller
+ here). They return the *complete* blocked-ray attack set in one
+ multiply+shift+double-indirect-load -- not just the nearest
+ blocker (`_WhoAttacksSquareBB`'s shape, built for a different
+ question). This is exactly the primitive section 3a of the old
+ draft assumed; it is no longer aspirational.
+2. **The motivating problem is now measured, not assumed.** Profiling
+ against Crafty on the same hardware (1.5Mnps vs. Crafty's 7.5Mnps,
+ both at a similar lazy-eval skip rate) points at `Eval()` itself,
+ not search, as the disproportionate cost. This plan exists to
+ attack that number directly, not as a speculative "bitboards are
+ modern, let's use them" exercise.
+
+Open question: both move gen (generate.c) and eval (eval.c) need a
+bbOccupied. Is this worth maintaining incrementally (in
+MakeMove/LiftPiece/SlidePiece/etc...) so that it will always be on
+POSITION and up-to-date?
+
+**The movegen project's own findings are the load-bearing precedent
+here, and they cut both ways -- worth stating plainly before
+proposing more work of the same shape:**
+
+- Most individual `_Generate*BB` functions landed at **speed parity**
+ with mailbox (0.71x-1.10x depending on position density), because a
+ mailbox ray walk's per-square cost was already close to
+ O(destination count) -- nothing wasted to reclaim.
+- The **real, consistent wins** were in a different place: the
+ dispatch layer (`_GenerateAllMovesBB` avoiding `JumpTable`'s
+ indirect, badly-predicted call, up to 23% in dense positions) and
+ `IsAttackedBB` (0.73x-0.93x, a genuine algorithmic improvement, not
+ just a constant factor, because mailbox `IsAttacked` is O(piece
+ count) and the bitboard version is closer to O(1)).
+- The stashed *first* Eval bitboard attempt regressed 17-41% for a
+ root cause fully diagnosed at the time: **pure duplication** --
+ every mobility function ran its full original ray-walk *and*
+ additionally wrote bitboards, so every position paid for both
+ representations and nothing was ever removed to pay for it. Not
+ evidence against this approach; evidence against ever doing it
+ additively again.
+
+Reading `Eval()`'s mobility loops against this precedent suggests
+`Eval()` is actually a *better* candidate for a bitboard win than move
+generation was, for a structural reason move generation never had:
+**every mobility loop today pays for an indirect `switch` dispatch per
+square visited** (`RMobCaseTable`/`BMobCaseTable`/`QMobCaseTable`/
+`NMobCaseTable`), explicitly chosen over a function-pointer jump table
+"to avoid an indirect call/ret... since the target piece varies square
+to square" (`_EvalBishop`'s own comment). A `switch` compiles to a
+jump table or branch chain that itself suffers exactly the
+misprediction problem `_GenerateAllMovesBB` was built to eliminate
+for move generation's dispatch layer -- and it runs *inside* the
+hottest loop in eval, once per square walked, not once per node. This
+plan's central bet is that removing this dispatch, not the ray-walk
+arithmetic itself, is where the win lives -- directly analogous to
+what actually paid off in the movegen project, not the part that
+didn't.
+
+## 0b. Performance philosophy for this rewrite -- what's negotiable
+and what isn't
+
+Stated by the user directly, 2026-09-05, and important enough to
+record verbatim as a standing constraint rather than let it live only
+in chat history: this engine runs at ~1.5Mnps against Crafty's
+~7.5Mnps on the same hardware, both profiled, and `Eval()` is the
+confirmed disproportionate cost -- not search. That changes the
+default posture for every decision in this plan:
+
+- **Benchmark everything, cut aggressively.** Every existing eval
+ term is a candidate for removal if it doesn't earn its cost --
+ section 7's cut list (dead `#if 0` code, max-mobility-in-a-row
+ bonus, bishop's transient-pawn credit, connected-rook x-ray
+ bookkeeping) should be treated as the *starting* set of suspects,
+ not an exhaustive one, and the DNA-zeroing sensitivity check +
+ cycle-cost measurement pairing should be run proactively across
+ `Eval()`'s terms rather than opportunistically on ones that already
+ look suspicious.
+- **The one thing that is not up for negotiation: attack-bitmap-driven,
+ mobility-and-safety-aware piece evaluation itself** -- not merely
+ "king safety," a narrower framing worth explicitly correcting here.
+ What today's `bvAttacks`/case-table machinery gives every piece
+ type is a single, unified mechanism for "does this piece actually
+ have useful, safe mobility given the current pawn structure and
+ attack picture" -- bad bishops, knights needing real outposts (not
+ just central *squares*, since a centralized knight throttled by an
+ enemy pawn chain gets no credit), rooks on genuinely open/contested
+ files, and king danger, all falling out of the same attack-bitmap
+ substrate rather than being separate hand-tuned heuristics. In the
+ user's own words: *"If I had to drop everything else from Eval to
+ afford this, I would still keep it. I believe it is what chess Eval
+ is about."* Section 2's replacement (`bbPawnAttacks`/
+ `bbMinorAttacks`/`bbRookAttacks`/`bbQueenAttacks` accumulators) is
+ in scope and *encouraged* precisely because it's the fast
+ replacement for this capability's current slow mechanism -- the
+ capability survives, only its implementation gets cheaper. What
+ must not happen is a cut that removes mobility/safety-awareness
+ itself in the name of speed (e.g. reverting a piece type to
+ raw material + PST with no attack-picture-conditioned mobility term
+ at all) -- every other term in `Eval()` is fair game for that kind
+ of cut; this one specifically is not.
+- **Profile before rewriting, not just before/after each toggle.**
+ Before continuing further piece-type work, get a per-eval-term
+ cycle-cost breakdown (perf counters or sampling, representative
+ position mix) of where `Eval()`'s total time actually goes today --
+ mobility ray-walks vs. pawn structure vs. king safety vs. everything
+ else. This should drive both rewrite sequencing and cut-candidacy;
+ section 4's knight-first ordering was sequenced by implementation
+ risk (fewest special cases), which is a reasonable tie-breaker but
+ shouldn't override what the actual profile says is worth attacking
+ first once that data exists.
+
+## 0c. `_EvalPawns` -- measured cost, and a concrete redundant-work
+finding (2026-09-05)
+
+The `EVAL_TIME` per-term instrumentation added per section 0b (see
+`chess.h`'s `u64CyclesEval*` counters, `root.c`'s breakdown print)
+gave a first real number: on one representative middlegame position
+(`sd 12`), `_EvalPawns` alone was **5.4% of total cycles spent in
+`Eval()`**, despite a measured pawn-hash hit rate above 99% -- i.e.
+this cost is overwhelmingly the *hit* path (hash probe + key compare
++ return), not the rare rebuild-on-miss path. Worth remembering for
+section 0b's "front-load if cheap, else estimate" framing: pawn
+structure is the *first* thing evaluated in `Eval()` today, which is
+right for a term this correlated with lazy-eval accuracy, but "first"
+doesn't mean "free" -- even a >99%-hit-rate cache probe measurably
+adds up at billions of calls, same lesson as the mobility dispatch
+switch in section 0.
+
+**Concrete, scoped finding on the miss path itself** (found by reading
+`_EvalPawns`, eval.c:1866 on, against `POSITION`'s own fields): on a
+pawn-hash miss, `_EvalPawns` rebuilds `pHash->bbPawnLocations[uColor]`
+bit-by-bit inside its per-pawn loop (`pHash->bbPawnLocations[uColor]
+|= COOR_TO_BB(c);`, eval.c:1958) by iterating `pos->cPawns[uColor][u]`
+-- but `POSITION` already carries `pos->bbPawns[2]`
+(`chess.h`:661-670), a plain, always-current, incrementally-maintained
+pawn-location bitboard (`move.c` updates it on every pawn move,
+`fen.c`/`board.c` build it at position-load time), entirely separate
+from the pawn-hash-keyed `pHash->bbPawnLocations[2]`. There's no
+reason for the miss path to reconstruct from scratch what's already
+sitting on `pos`: `pHash->bbPawnLocations[uColor] =
+pos->bbPawns[uColor];` once per color replaces the bit-by-bit OR
+inside the loop. **The loop itself still has to run** for the
+per-file counts (`pHash->uCountPerFile`) and whatever isolated/
+doubled/duo/passed-pawn detection follows -- this only removes the
+bitboard-population part of that loop's work, not the loop -- and
+since it's gated behind the <1%-of-calls miss path, the aggregate win
+is real but necessarily small. Flagged here as a correctness-safe,
+low-risk cut to take regardless of the rest of this plan's sequencing,
+not because it's expected to move the 5.4% number much on its own.
+
+**Explicit scope note, stated by the user directly**: `_EvalPawns`
+stays pawn-structure-only -- passed-pawn detection, connectivity, and
+whatever else it currently does for pawns are all still wanted. The
+ask here is narrower than "simplify pawn eval": stop redoing bitboard
+work `POSITION` already maintains for you, don't cut pawn eval terms.
+
+Every one of `_EvalBishop`/`_EvalKnight`/`_EvalRook`/`_EvalQueen` was
+read end to end for this rewrite (`eval.c`). Two clearly separable
+halves exist in every one of them:
+
+### 1a. Non-mobility terms -- already fine, not a target
+
+Open/half-open file bonuses, rook-behind/leads-passer, knight
+outpost/tropism, bishop good/bad-pawn-color counting, centrality,
+closed-position scalers -- all of these read `pHash`-cached bitboards
+(`bbPasserLocations`, `bbStationaryPawns`, `bbPawnLocations`,
+`uCountPerFile`) or O(1) `POSITION` fields. No ray-walk, no per-square
+switch, already bitboard-driven where it needs to be. **Do not touch
+these** -- rewriting them buys nothing and only adds risk surface.
+
+### 1b. Mobility ray-walks -- the actual target, and the switch is
+the point, not the walk
+
+All four piece types share the same shape: walk each ray direction
+square by square, at every square (1) OR a bit into
+`pos->rgSquare[cSquare|8].bvAttacks[uColor]` (see section 2), (2)
+dispatch on `pos->rgSquare[cSquare].pPiece` via a per-color 14-entry
+case table to decide "count this square? keep walking? x-ray past
+it?". Read closely, the case tables collapse to a small, enumerable
+set of primitive facts once you stop thinking per-square and start
+thinking per-ray-segment:
+
+**Rook (`RMobCaseTable`, 6 live cases):**
+- `RMOB_EMPTY` / `RMOB_ENEMY_LESS` (any enemy piece, actually --
+ "less" is a stale name, the table just needs "count if safe, stop
+ if occupied"): count if `!UNSAFE_FOR_ROOK`, stop if occupied.
+- `RMOB_FRIEND_BLOCK` (friendly non-rook/queen): stop, no count.
+- `RMOB_FRIEND_ROOK` / `RMOB_FRIEND_QUEEN`: don't stop -- x-ray past a
+ same-color battery partner, no mobility credit at the blocker
+ itself, award `ROOK_CONNECTED_HORIZ`/`_VERT` at the blocker square.
+- `RMOB_ENEMY_SAME` / `RMOB_ENEMY_GREATER`: count (unconditionally,
+ no unsafe-check -- this is deliberate, capturing an equal/higher
+ rook/queen/king is never "unsafe" in the sense mobility cares
+ about), stop.
+
+Reduction: `bbAttack = _RookAttacksBB(c, bbOccupied)` already stops at
+the nearest blocker in every direction by construction -- that
+blocker is exactly the one square each case above is examining.
+`uTotalMobility = CountBits((bbAttack & ~bbFriendlyOccupied &
+~bbUnsafeForRook) | bbEnemyBlockerCredit)`, where `bbEnemyBlockerCredit`
+handles the fact that an occupied-by-enemy terminal square counts
+*without* the unsafe check (`RMOB_ENEMY_SAME`/`_GREATER` never test
+`UNSAFE_FOR_ROOK`, only `RMOB_EMPTY`/`_ENEMY_LESS` -- i.e. the
+non-terminal-and-terminal-enemy cases both existed in the original
+table, but only the *empty-square* case is masked by
+`bbUnsafeForRook`; every enemy-occupied terminal square counts
+unconditionally regardless of case name). Concretely:
+
+```c
+bbAttack = _RookAttacksBB(c, bbOccupied);
+bbEnemy = bbAttack & bbEnemyOccupied; /* terminal enemy squares */
+bbEmpty = bbAttack & ~bbOccupied; /* non-terminal empty squares */
+bbMobility = bbEnemy | (bbEmpty & ~bbUnsafeForRook);
+uTotalMobility = CountBits(bbMobility);
+```
+
+The friendly-rook/queen x-ray case needs its own handling since a
+magic lookup's attack set stops *at* the blocker regardless of type,
+giving no visibility past it: detect `bbFriendRQ = bbAttack &
+(pos->bbPieces[uColor][ROOK] | pos->bbPieces[uColor][QUEEN])`
+(0 or 1 bits, essentially always), and only when nonzero, recompute
+`_RookAttacksBB(c, bbOccupied & ~bbFriendRQ)` restricted to the same
+ray (`g_RookRayToEdge[dir][c]`) to get the far-side squares for the
+attack-bit population in section 2 -- this does not change
+`uTotalMobility` (the original code never credited mobility past a
+friendly battery partner either). The connected-rook bonus itself
+(`ROOK_CONNECTED_HORIZ`/`_VERT`) needs to know if the ray to the
+blocker was horizontal or vertical, which is already known for free
+from *which* of the 4 ray directions found it.
+
+**Per-direction max mobility**: `CountBits(bbMobility &
+g_RookRayToEdge[dir][c])` for each of 4 directions -- 4 extra popcounts,
+still branch-free, no walk.
+
+**Bishop (`BMobCaseTable`, 7 live cases) -- one genuine wrinkle rook
+doesn't have:**
+
+- `BMOB_EMPTY` / `BMOB_ENEMY_PAWN`: same shape as rook's
+ empty/enemy-less, masked by `UNSAFE_FOR_MINOR`.
+- `BMOB_FRIEND_BLOCK`: stop, no count (friendly knight/rook/king).
+- `BMOB_FRIEND_PAWN`: stop, **but still counts as 1 mobility square
+ if `pos->bb & COOR_TO_BB(cSquare)`** -- `pos->bb` here is a
+ *scratch* alias set to "pawns of either color, non-stationary,
+ restricted to this bishop's own color complex" just before the walk
+ begins (`pos->bb = bbPc`, see the code right above the ray loop).
+ This is real, intentional signal (a friendly *transient* pawn -- not
+ rammed/backward -- sitting on the bishop's own diagonal still
+ "counts" toward mobility, on the theory it'll likely move and open
+ the diagonal soon) and is **the one case that isn't a pure
+ `~bbFriendlyOccupied` mask** -- it needs `bbAttack`'s terminal
+ friendly-pawn bit added back in when that pawn is transient and on
+ the bishop's own color. Since at most one square per ray direction
+ can be this case (the walk stops there), this is a cheap
+ `bbAttack & pos->bbPawns[uColor] & bbTransientOwnColorPawns` term
+ ORed into the mobility mask, not a structural problem -- just a
+ named exception that must not be dropped silently during the
+ rewrite.
+- `BMOB_ENEMY_SAME` (opposing bishop/knight): count, stop.
+- `BMOB_FRIEND_XRAY` (friendly bishop/queen): x-ray past, no count,
+ same shape as rook's battery case.
+- `BMOB_ENEMY_GREATER` (enemy rook/queen/king): **count, then x-ray
+ past anyway** -- unlike rook, bishop's x-ray set includes both the
+ same-type friendly case *and* an enemy-major-piece case. This means
+ the "recompute with blocker excluded" trick (needed for attack-bit
+ population past the blocker, section 2) has to run for *two*
+ distinct terminal-piece categories here, not one.
+
+**Queen (`QMobCaseTable`, 7 live cases) -- mechanically rook+bishop,
+with one extra wrinkle from combining two ray families:**
+
+- Same empty/enemy-less/friend-block/enemy-same shape as rook, masked
+ by `UNSAFE_FOR_QUEEN`.
+- `QMOB_FRIEND_BISHOP` x-rays only on a diagonal ray, blocks on an
+ orthogonal one (`fStop = fOrthogonalRay`); `QMOB_FRIEND_ROOK` is the
+ mirror (`fStop = !fOrthogonalRay`). This is already exactly what
+ computing queen mobility as two separate rook-direction/
+ bishop-direction magic lookups naturally gives for free -- no new
+ logic needed, `fOrthogonalRay`'s role is entirely subsumed by "which
+ of the two lookups this ray direction belongs to."
+- `QMOB_FRIEND_QUEEN`: x-ray past (either ray family), same as rook's
+ own-type case.
+- `QMOB_ENEMY_GE` (any enemy piece): count, stop, **no x-ray past** --
+ unlike bishop's `BMOB_ENEMY_GREATER`, queen does not x-ray through a
+ captured-but-higher-value enemy. One fewer special case than bishop,
+ not more.
+
+Reduction: `bbAttack = _RookAttacksBB(c, occ) | _BishopAttacksBB(c,
+occ)`, each masked/unsafe-checked with its own family's mask
+(`bbUnsafeForQueen` uniformly, since the mailbox code doesn't
+distinguish rook-direction vs. bishop-direction safety for the
+queen), matching `MOVEGEN_MIGRATION.md` section 3's own already-tested
+"combined 8-ray table measured slower than reusing the rook/bishop
+tables in two passes" finding -- reuse that two-pass structure here
+too, don't rediscover the same regression.
+
+**Knight (`NMobCaseTable`, 3 live cases) -- the simple case, confirmed
+by direct read:**
+
+- `NMOB_MOBILE_SQUARE` (empty or enemy pawn): count if
+ `!UNSAFE_FOR_MINOR`.
+- `NMOB_ENEMY_OTHER` (any other enemy piece): count, unconditionally.
+- `NMOB_FRIEND`: no count.
+
+No terminal-blocker subtlety at all (knights don't block through
+anything). Reduction is exactly one line:
+
+```c
+bbAttack = g_KnightAttacksBB[c];
+bbMobility = (bbAttack & bbEnemyNonPawnOccupied) |
+ ((bbAttack & ~bbFriendlyOccupied & ~bbEnemyNonPawnOccupied)
+ & ~bbUnsafeForMinor);
+uMobilitySquares = CountBits(bbMobility);
+```
+
+(the split exists only because `NMOB_ENEMY_OTHER` skips the unsafe
+check and `NMOB_MOBILE_SQUARE` doesn't -- can likely simplify further
+once `bbEnemyNonPawnOccupied`'s exact membership is nailed down
+against the case table above).
+
+### 1c. What every one of these functions does *after* mobility --
+unaffected
+
+Trapped-piece recording (`_RecordTrappedCandidate`, gated on
+`uTotalMobility == 0`/`< 3`) and the rook-corner king-trap check are
+already independent of *how* the mobility count was computed --
+feed them the bitboard-computed number, no rewrite needed.
+
+## 2. `bvAttacks` replacement -- still the load-bearing new
+infrastructure, design unchanged from the first draft, now
+concretely groundable against real code
+
+`ATTACK_BITV` (`chess.h`) packs presence bits per piece-type family
+into one `ULONG` per square per color half
+(`pos->rgSquare[c|8].bvAttacks[color]`), with `UNSAFE_FOR_MINOR`/
+`_ROOK`/`_QUEEN` macros masking the relevant bits
+(`0x80`/`0xC0`/`0xE0` -- i.e. each wider piece type's "am I safe here"
+question is a superset of the narrower one's, matching chess reality:
+a queen cares about more attackers than a rook, which cares about more
+than a minor). This is read constantly (every mobility loop's
+`UNSAFE_FOR_X` check) and written constantly (every mobility loop's
+`|= uBit`), in a strict piece-evaluation order that later pieces
+depend on.
+
+Replacement, same shape as the first draft proposed, now stated
+precisely against the actual bit semantics above:
+
+- `Eval()`-local (not `POSITION`-resident -- same call-scoped-lifetime
+ discipline `MOVEGEN_MIGRATION.md` section 3c/6b already established
+ for its own scratch bitboards) per-side accumulators:
+ `bbPawnAttacks[2]`, `bbMinorAttacks[2]` (knight | bishop, matching
+ `UNSAFE_FOR_MINOR`'s own single-tier mask), `bbRookAttacks[2]`,
+ `bbQueenAttacks[2]`.
+- Populated in `Eval()`'s existing piece-evaluation order (pawns,
+ then knights/bishops, then rooks, then queens, then king), each
+ piece type OR-ing its own `bbAttack` (the same value computed for
+ its own mobility, no second computation) into its side's
+ accumulator once, immediately after that piece's own mobility is
+ scored.
+- `bbUnsafeForMinor` at knight/bishop-evaluation time =
+ `bbPawnAttacks[enemy]` (matches the `0x80` mask -- only pawns make a
+ square unsafe for a minor). `bbUnsafeForRook` = `bbPawnAttacks[enemy]
+ | bbMinorAttacks[enemy]` (`0xC0`). `bbUnsafeForQueen` adds
+ `bbRookAttacks[enemy]` (`0xE0`). Each is a single OR of
+ already-populated accumulators, computed once per side per `Eval()`
+ call -- not once per candidate square, a strict improvement over
+ today's per-square `ATTACK_BITV` read.
+- **King safety zone queries** (`_EvalKing`'s read side, section 3
+ below) become `CountBits(bbXAttacks[enemy] & kingZoneMask)` directly
+ against these same accumulators -- no separate data structure
+ needed for that consumer.
+
+One correction versus the first draft's framing: `pos->rgSquare[]`
+itself is **not fully retirable** even after this lands, for a reason
+the movegen project's own `movesup.c` survey already surfaced --
+every generator, bitboard or not, still needs `pos->rgSquare[c].pPiece`
+to answer "what's on the terminal/blocker square" (for the
+enemy-vs-friendly, same-vs-greater-value classification every piece
+type's reduction above still needs at the *one* terminal square per
+ray, not for the whole ray). This plan retires the mailbox *ray-walk
+and the bvAttacks per-square bitfield*, not the mailbox array itself.
+
+## 3. `CountKingSafetyDefects`/`_EvalKing` -- unchanged scope from the
+first draft, still explicitly separate
+
+`_EvalKing`'s *read* side (walking `KingSafetyDeltas` around the
+king, summing attacker/defender presence) becomes a `CountBits`
+query against section 2's accumulators, a pure consumer change.
+`CountKingSafetyDefects` itself is **not** a port target -- per the
+first draft's already-recorded decision, it gets a from-scratch
+bitboard-native reimplementation that need not preserve exact
+semantics (allowed to be *more accurate*, since it's a lazy-eval
+magnitude hint, not a scored term itself), scoped and gated entirely
+separately from the mobility rewrite below. Do not bundle it into the
+same toggle or the same benchmark pass.
+
+**Correction, found by reading the actual call sites (not by
+analogy): `CountKingSafetyDefects` cannot consume section 2's
+accumulators even opportunistically, and its role is bigger than "a
+lazy-eval magnitude hint."**
+
+1. **Ordering makes reuse structurally impossible, not just
+ undesirable.** `CountKingSafetyDefects` runs during `Eval()`'s
+ lazy-eval margin phase (`EstimatePositionalScore`, and the
+ `CountKingSafetyDefects(pos, WHITE/BLACK)` refresh calls at
+ eval.c:4976-4977) -- strictly *before* the piece-eval loop that
+ builds `bvAttacks` today, and before section 2's `bbPawnAttacks`/
+ `bbMinorAttacks`/`bbRookAttacks`/`bbQueenAttacks` accumulators
+ would exist under this plan (pawns -> knights/bishops -> rooks ->
+ queens -> king). There is no rewrite of `CountKingSafetyDefects`
+ that can read those accumulators; by construction they aren't
+ populated yet at the point it's called. It has to remain what it
+ already is: a geometry-only estimate over enemy piece *locations*
+ relative to the king (`CHECK_VECTOR_WITH_INDEX`), with no attack
+ data available, bitboard-native or otherwise.
+2. **It is not only a lazy-eval input.** `search.c:1252`
+ (`CountKingSafetyDefects(pos, pos->uToMove) > 2`) and
+ `searchsup.c:484` (`CountKingSafetyDefects(&ctx->sPosition, uColor)
+ > 1`) call it directly as a search-time extension/reduction gate,
+ fully independent of `Eval()`'s lazy-exit path. A drift here
+ doesn't just blur a margin estimate -- it changes which nodes get
+ extended or reduced at every level of the tree.
+
+**The actual constraint this plan needs, stated precisely**: not
+"port `CountKingSafetyDefects` to bitboards," but *"whichever accuracy
+improvements section 3's `_EvalKing` rewrite introduces (e.g. properly
+catching x-ray/latent queen threats via the new accumulators),
+`CountKingSafetyDefects`'s cheap geometric estimate must remain
+correlated with `_EvalKing`'s real danger score across the position
+space."* Concretely, the two thresholds already load-bearing in
+production (`> 1` in `searchsup.c`, `> 2` in `search.c`) need to keep
+firing on roughly the same set of positions `_EvalKing`'s new,
+possibly-more-accurate score would flag as dangerous. If `_EvalKing`
+gets meaningfully better while `CountKingSafetyDefects` stays exactly
+as it is today, that correlation can silently degrade even though
+neither function, read in isolation, has a bug -- the failure mode is
+between them, not inside either one.
+
+**Added verification step (section 5 should include this, not just
+`_EvalKing`'s own exact-score harness)**: over the same 20,000-position
+`GenerateRandomLegalPosition` sample, compute both
+`CountKingSafetyDefects(side)` (old, unchanged) and the new
+`_EvalKing`'s real danger score, old vs. new, and check:
+
+- **Correlation, not equality** -- these were never meant to match
+ exactly (one is a cheap O(pieces) geometric proxy, the other a full
+ bitboard-weighted computation); the thing to track is whether their
+ relative ranking of "how dangerous is this king position" stays
+ consistent before vs. after the `_EvalKing` rewrite.
+- **Threshold agreement specifically at the two live gates** -- for
+ each sampled position, does `CountKingSafetyDefects(side) > 1` (and
+ `> 2`) still agree with "old `_EvalKing` considered this position
+ dangerous" at roughly the same rate it agrees with "new `_EvalKing`
+ considers this position dangerous"? A meaningful shift in agreement
+ rate, not just a raw score delta, is the signal that
+ `CountKingSafetyDefects` itself may need re-tuning (its
+ `KingFlightDefects`/`KingStormingPawnDefects`/`CHECK_VECTOR`
+ constants are DNA-tunable, per `eval_tune/`) to stay a useful proxy
+ for the new, more accurate ground truth -- not evidence that the
+ `_EvalKing` rewrite itself is wrong.
+- **Downstream check**: re-run `match_play.py` and the curated-suite
+ `sd10` gate (section 5.2/5.3) with `_EvalKing`'s rewrite landed but
+ *before* touching `CountKingSafetyDefects`'s constants, specifically
+ watching for search-behavior regressions (unexpected extension/
+ reduction pattern shifts) that a pure eval-score comparison would
+ miss, since sections 5.1-5.3 as currently scoped only compare
+ `Eval()`'s output, not `CountKingSafetyDefects`'s search-time
+ pruning effect.
+
+## 4. Toggle strategy -- learn from the stash's mistake explicitly
+
+One `#define` per piece type (`EVAL_KNIGHT_BITBOARD`/
+`EVAL_BISHOP_BITBOARD`/`EVAL_ROOK_BITBOARD`/`EVAL_QUEEN_BITBOARD`),
+matching the movegen project's per-piece-type convention -- **but each
+toggle must replace that piece type's entire mobility+attack-bit
+computation, never add a bitboard computation alongside the mailbox
+one.** This is not a style preference; it is the single, fully
+diagnosed cause of the stashed attempt's 17-41% regression, and must
+be treated as a hard constraint on every toggle in this plan, not a
+lesson to merely keep in mind. Section 2's `bbUnsafeFor*`
+infrastructure is shared prerequisite state, needed before *any*
+piece type's toggle can be flipped on -- there's no meaningful
+partial-rollout ordering where accumulator infrastructure lands after
+a consumer.
+
+Suggested order, cheapest/most-precedented first (matches
+`MOVEGEN_MIGRATION.md`'s own successful sequencing rationale):
+knight (no terminal-blocker special case at all) -> rook (one x-ray
+category) -> bishop (two x-ray categories, one non-mask-reducible
+transient-pawn credit) -> queen (mechanical once rook+bishop are
+proven, reuses their two-pass structure).
+
+## 5. Correctness verification
+
+Same bar as `MOVEGEN_MIGRATION.md`, arguably higher since `Eval()`
+feeds every leaf score in the tree rather than just node
+ordering/generation:
+
+1. **Exact-score comparison harness**: for each of
+ `GenerateRandomLegalPosition`'s 20,000-position sample (same source
+ the movegen project's own harness used), compute the specific
+ `EVAL_TERM` contributions this plan touches (mobility +
+ connected/x-ray bonuses + trapped-candidate flag) old vs. new,
+ per piece type independently (mirrors the movegen project's
+ per-piece-type toggle granularity in the harness too). Must be
+ byte-identical -- `Eval()` is deterministic-by-design, any mismatch
+ is a real bug.
+2. **Whole-engine `sd10` on all three curated suites vs.
+ `head_reference`** -- rebuilt post-movegen-landing baseline, so
+ this plan's own gate isn't contaminated by the movegen commit's
+ own (already-confirmed-unrelated) `ecm_hard_quick` drift.
+3. **`match_play.py`** (`LOWER95 >= 0.5`), same gate, same reasoning:
+ given the movegen project's own experience that most individual
+ functions land near parity, expect this gate to converge toward
+ "no regression" rather than a large positive swing, and don't
+ over-read a near-0.5 LLR as a problem the way the movegen SPRT run
+ didn't need to either.
+4. **`precommit_check.sh`** as always, for the crash/assert layer.
+
+## 6. Microbenchmarking
+
+Same two-tier approach as every prior plan in this family:
+
+1. **Isolated cycles/call per piece type**, interleaved old/new,
+ across opening/middlegame/endgame density, modeled on
+ `testgenerate.c`'s existing methodology (new `testeval.c`-resident
+ harness, or extend the existing one). Given section 0's
+ dispatch-elimination bet, pay particular attention to whether the
+ win (if any) tracks with *how many pieces/squares* a position has
+ to walk (more squares -> more switch mispredictions saved) rather
+ than being flat -- a density-correlated win would confirm the
+ dispatch-removal theory; a flat or absent one would suggest the
+ `switch` was actually being predicted fine and the bet was wrong.
+2. **Whole-`Eval()` cycles/call**, same interleaved technique -- the
+ number that actually matters, since mobility is only part of
+ `Eval()`'s total cost.
+3. **Whole-engine NPS** on the curated suites, and directly against
+ the original motivating comparison: **re-run the same Crafty
+ side-by-side NPS comparison that motivated this plan**, not just
+ an internal before/after -- the actual goal is closing (some of)
+ the 1.5M-vs-7.5Mnps gap, and that number is the one that should
+ ultimately justify the effort here, not an isolated microbenchmark
+ in a vacuum.
+
+## 7. What to consider cutting, not just speeding up
+
+Raised at the user's request -- anything that resists a clean bitboard
+reduction is a candidate for this discussion, not an automatic
+"implement it the hard way":
+
+- **Dead code, delete regardless of this plan**: every `#if 0`
+ min-mobility-tracking block in `_EvalRook`/`_EvalBishop`/
+ `_EvalQueen`/`_EvalKnight` -- unused (`pos->uMinMobility` is written,
+ never read live), pure removal, zero risk, zero dependency on
+ anything else in this plan.
+- **Per-direction max-mobility bonus**
+ (`ROOK_MAX_MOBILITY_IN_A_ROW_BONUS`/`BISHOP_MAX_MOBILITY_IN_A_ROW_BONUS`) --
+ survives the rewrite as 4 extra `CountBits` calls per slider, cheap
+ either way, but worth a DNA-tuning-style sensitivity check (does
+ zeroing this table's contribution move solve rate or match score at
+ all?) independent of the bitboard work -- if it's not pulling
+ weight, cutting it removes both eval-time cost and tuning-parameter
+ count for free. Not blocking this plan; worth doing opportunistically
+ once the rewrite is in place and the harness exists to check it
+ cheaply.
+- **Bishop's `BMOB_FRIEND_PAWN` transient-pawn mobility credit**
+ (section 1b) -- the single case that doesn't reduce to a mask
+ operation as cleanly as everything else. It's cheap to keep (one
+ extra AND term), but if the isolated-benchmark step shows bishop's
+ reduction costing more than knight/rook/queen's for a
+ disproportionately small eval-quality contribution, this specific
+ case is the one to question first, precisely because it's the one
+ genuinely bespoke piece of logic in the whole rewrite. Measure
+ before deciding, per section 6 -- don't cut preemptively.
+- **`CountKingSafetyDefects`** -- not a "cut" candidate, but flagged
+ again here because it's the one function in this whole area that
+ isn't a straightforward port either way (section 3); if the
+ from-scratch reimplementation turns out to be disproportionately
+ expensive to get right relative to its actual scoring impact, that's
+ a legitimate place to ask "is a simpler, less-accurate hint good
+ enough" rather than chasing full replacement fidelity.
+- **Connected-rook / bishop-x-ray attack-bit population past a
+ blocker** (section 1b's "recompute occupancy with blocker excluded"
+ trick) -- this exists purely to keep populating `bvAttacks`-derived
+ king-safety-relevant bits correctly for squares beyond a battery
+ partner. If, once section 2's accumulators are live and
+ `_EvalKing`'s consumption is measured, this far-side attack
+ information turns out to contribute negligibly to king-safety
+ scoring in practice (worth checking via the same DNA-sensitivity
+ technique), it's a legitimate cut -- dropping it would remove the
+ single most annoying piece of bookkeeping in this entire plan (the
+ two-terminal-category case for bishop, section 1b) for what might be
+ a very small eval-quality cost. Explicitly flagged as the highest-
+ complexity-per-value item in the plan; measure before committing to
+ full fidelity.
+
+## 8. Retirement criteria
+
+Same shape as `MOVEGEN_MIGRATION.md` section 7: per piece type, delete
+that type's mailbox mobility ray-walk only after the exact-score
+harness (section 5.1), the `sd10` curated-suite check (section 5.2),
+the isolated-cycles benchmark (section 6.1) showing a consistent win
+(or an explicit decision to keep it at parity for the dispatch-layer
+win, same reasoning the movegen project used for its own near-parity
+functions), and `match_play.py` (section 5.3) all pass. Don't let
+knight's likely-clean bill of health lower the bar for bishop's
+two-x-ray-category case or queen's combined-ray-family case --
+each has different enough special-case surface (section 1b) to
+warrant its own full pass through this list.
diff --git a/src/chess.h b/src/chess.h
index ed903a5..f39a956 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -653,9 +653,8 @@ typedef struct _POSITION
// convention). Maintained incrementally in move.c at every site
// that already updates cNonPawns[]/uNonPawnCount[] (see
// board_representation/MIGRATION.md), not rebuilt -- O(1) per
- // move. Pawns use pHash->bbPawnLocations[2] (pawn-hash-keyed,
- // already established) instead; king is a single square
- // (cNonPawns[color][0]), a bitboard adds nothing.
+ // move. Pawns use bbPawns[2] below instead; king is a single
+ // square (cNonPawns[color][0]), a bitboard adds nothing.
BITBOARD bbPieces[2][8];
// Per-color pawn location bitboard -- same incremental-maintenance
@@ -663,10 +662,13 @@ typedef struct _POSITION
// 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.
+ // board_representation/MIGRATION.md section 3. Also the single
+ // source of truth for pawn locations used by pawn eval
+ // (board_representation/EVAL.md section 0c) -- PAWN_HASH_ENTRY
+ // used to carry its own redundant bbPawnLocations[2], rebuilt
+ // bit-by-bit on every pawn-hash miss even though this field
+ // already had the answer; removed 2026-09-05, eval.c now reads
+ // this field directly instead.
BITBOARD bbPawns[2];
ULONG uWhiteSqBishopCount[2]; // num bishops on white squares
@@ -896,6 +898,40 @@ typedef struct _COUNTERS
UINT64 u64LazyEvals;
UINT64 u64FullEvals;
UINT64 u64CyclesInEval;
+
+ //
+ // Per-eval-term cycle breakdown (EVAL_TIME only) -- board_
+ // representation/EVAL.md section 0b's "profile before
+ // rewriting" step. Sum of these plus "everything else"
+ // (u64CyclesInEval minus this sum, computed at print time)
+ // equals u64CyclesInEval; kept as separate counters rather
+ // than an array so each term's call sites in eval.c can name
+ // its own accumulator directly.
+ //
+ UINT64 u64CyclesEvalPawns;
+ UINT64 u64CyclesEvalKnight;
+ UINT64 u64CyclesEvalBishop;
+ UINT64 u64CyclesEvalRook;
+ UINT64 u64CyclesEvalQueen;
+ UINT64 u64CyclesEvalKing;
+
+ //
+ // u64CyclesEvalPreLazy covers the "always paid, even on a
+ // lazy exit" segment -- material setup through the lazy-
+ // margin decision (passer races, bad trades, bishop pairs,
+ // EstimatePositionalScore/CountKingSafetyDefects) -- and
+ // *includes* u64CyclesEvalPawns as a subset (pawns is nested
+ // inside this segment, not sequential with it); print sites
+ // must subtract pawns back out rather than summing both.
+ // u64CyclesEvalPostLazyMisc covers what's left of the "only
+ // paid on a full eval" segment once the named piece-type
+ // buckets above are excluded: _EvalPassers, _EvalLookForDanger,
+ // _EvalTrappedPieces, the B-over-N/reduced-material endgame
+ // scalers.
+ //
+ UINT64 u64CyclesEvalPreLazy;
+ UINT64 u64CyclesEvalPostLazyMisc;
+ UINT64 u64CyclesEvalAttackTablePop;
}
tree;
@@ -1035,7 +1071,6 @@ PLY_INFO;
typedef struct _PAWN_HASH_ENTRY
{
UINT64 u64Key;
- BITBOARD bbPawnLocations[2];
BITBOARD bbPasserLocations[2];
BITBOARD bbStationaryPawns[2];
SHORT iScore[2];
@@ -3042,6 +3077,29 @@ TestEvalWithSymmetry(void);
#endif
//
+// Per-eval-term cycle accounting, EVAL_TIME only -- see
+// board_representation/EVAL.md section 0b. CTX must be the
+// SEARCHER_THREAD_CONTEXT* in scope at the call site; COUNTER names
+// one of the ctx->sCounters.tree.u64CyclesEval* fields. Wraps CALL
+// (an expression, e.g. a function call whose return value is
+// discarded or assigned outside the macro) with a start/stop
+// timestamp pair, accumulating elapsed cycles into COUNTER. A no-op
+// wrapper (CALL alone) outside EVAL_TIME builds so call sites don't
+// need their own #ifdef.
+//
+#ifdef EVAL_TIME
+#define TIMED_EVAL_CALL(CTX, COUNTER, CALL) \
+ do { \
+ UINT64 u64EvalTimerStart = SystemReadTimeStampCounter(); \
+ CALL; \
+ (CTX)->sCounters.tree.COUNTER += \
+ (SystemReadTimeStampCounter() - u64EvalTimerStart); \
+ } while (0)
+#else
+#define TIMED_EVAL_CALL(CTX, COUNTER, CALL) CALL
+#endif
+
+//
// bitboard.c
//
void
diff --git a/src/eval.c b/src/eval.c
index 23f830c..17793da 100755
--- a/src/eval.c
+++ b/src/eval.c
@@ -1344,7 +1344,7 @@ Return value:
if (pHash->uCountPerFile[FLIP(uColor)][uPawnFile] != 0)
{
- ASSERT((pHash->bbPawnLocations[FLIP(uColor)] & BBFILE[FILE(c)]) != 0);
+ ASSERT((pos->bbPawns[FLIP(uColor)] & BBFILE[FILE(c)]) != 0);
//
// The only way a pawn can be a passer/candidate if the other
@@ -1355,7 +1355,7 @@ Return value:
switch(uColor)
{
case WHITE:
- bb = pHash->bbPawnLocations[BLACK] & BBFILE[FILE(c)];
+ bb = pos->bbPawns[BLACK] & BBFILE[FILE(c)];
ASSERT(bb);
while(IS_ON_BOARD(c1 = CoorFromBitBoardRank8ToRank1(&bb)))
{
@@ -1364,7 +1364,7 @@ Return value:
}
break;
case BLACK:
- bb = pHash->bbPawnLocations[WHITE] & BBFILE[FILE(c)];
+ bb = pos->bbPawns[WHITE] & BBFILE[FILE(c)];
ASSERT(bb);
while(IS_ON_BOARD(c1 = CoorFromBitBoardRank1ToRank8(&bb)))
{
@@ -1380,7 +1380,7 @@ Return value:
// critical square. Note if there are no sentries then this
// pawn (on square c) is a passer already, not a candidate.
//
- bb = pHash->bbPawnLocations[FLIP(uColor)] & BBADJACENT_FILES[FILE(c)];
+ bb = pos->bbPawns[FLIP(uColor)] & BBADJACENT_FILES[FILE(c)];
bb &= BBPRECEEDING_RANKS[(c & 0x70) >> 4][uColor];
if (!bb)
{
@@ -1487,7 +1487,7 @@ Return value:
c1 = cSquare + 1 - d1;
if ((IS_ON_BOARD(c1)) && (pHash->uCountPerFile[uColor][FILE(c1) + 1]))
{
- ASSERT(pHash->bbPawnLocations[uColor] & BBFILE[FILE(c1)]);
+ ASSERT(pos->bbPawns[uColor] & BBFILE[FILE(c1)]);
if (!(pos->rgSquare[c1 + 8].bvAttacks[FLIP(uColor)].uWholeThing) ||
(pos->rgSquare[c1 + 8].bvAttacks[uColor].uWholeThing))
{
@@ -1528,7 +1528,7 @@ Return value:
c1 = cSquare - 1 - d1;
if ((IS_ON_BOARD(c1)) && (pHash->uCountPerFile[uColor][FILE(c1) + 1]))
{
- ASSERT(pHash->bbPawnLocations[uColor] & BBFILE[FILE(c1)]);
+ ASSERT(pos->bbPawns[uColor] & BBFILE[FILE(c1)]);
if (!(pos->rgSquare[c1 + 8].bvAttacks[FLIP(uColor)].uWholeThing) ||
(pos->rgSquare[c1 + 8].bvAttacks[uColor].uWholeThing))
@@ -1721,7 +1721,7 @@ Return value:
{
if (pHash->uCountPerFile[FLIP(uColor)][u + 1] != 0)
{
- ASSERT(pHash->bbPawnLocations[FLIP(uColor)] & BBFILE[u]);
+ ASSERT(pos->bbPawns[FLIP(uColor)] & BBFILE[u]);
if (!(pHash->bbPasserLocations[FLIP(uColor)] & BBFILE[u]))
{
break;
@@ -1742,7 +1742,7 @@ Return value:
{
if (pHash->uCountPerFile[FLIP(uColor)][u + 1] != 0)
{
- ASSERT(pHash->bbPawnLocations[FLIP(uColor)] & BBFILE[u]);
+ ASSERT(pos->bbPawns[FLIP(uColor)] & BBFILE[u]);
if (!(pHash->bbPasserLocations[FLIP(uColor)] & BBFILE[u]))
{
break;
@@ -1950,15 +1950,18 @@ Return value:
pHash->uCountPerFile[uColor][uPawnFile]++;
//
- // Update bitboard bit
+ // pos->bbPawns[uColor] is plain POSITION state, maintained
+ // incrementally by move.c on every pawn move -- already
+ // has this pawn's bit set by the time we get here, nothing
+ // to build. (No running-count cross-check against
+ // uCountPerFile here: unlike the old bit-by-bit
+ // pHash->bbPawnLocations this replaces, bbPawns already
+ // holds every pawn on the board up front, not just the
+ // ones this loop has visited so far, so a per-iteration
+ // "counts so far agree" comparison isn't meaningful
+ // against it -- only a post-loop total would be.)
//
- ASSERT(CountBits(pHash->bbPawnLocations[uColor] &
- BBFILE[uPawnFile-1]) < 6);
- ASSERT((pHash->bbPawnLocations[uColor] & COOR_TO_BB(c)) == 0);
- pHash->bbPawnLocations[uColor] |= COOR_TO_BB(c);
- ASSERT(CountBits(pHash->bbPawnLocations[uColor] &
- BBFILE[uPawnFile-1]) ==
- pHash->uCountPerFile[uColor][uPawnFile]);
+ ASSERT(pos->bbPawns[uColor] & COOR_TO_BB(c));
//
// Count unmoved pawns
@@ -2007,7 +2010,7 @@ Return value:
{
ASSERT(IS_VALID_COLOR(uColor));
ASSERT(pos->uPawnCount[uColor] <= 8);
- ASSERT(CountBits(pHash->bbPawnLocations[uColor]) <= 8);
+ ASSERT(CountBits(pos->bbPawns[uColor]) <= 8);
d1 = 16 * g_iAhead[uColor];
for (u = 0;
@@ -2029,12 +2032,12 @@ Return value:
uPawnFile = FILE(c) - 1;
if (pHash->uCountPerFile[uColor][uPawnFile + 1] > 0)
{
- bb = (pHash->bbPawnLocations[uColor] &
+ bb = (pos->bbPawns[uColor] &
BBFILE[uPawnFile] &
BBADJACENT_RANKS[RANK(c)]);
if (!bb)
{
- bb = (pHash->bbPawnLocations[FLIP(uColor)] &
+ bb = (pos->bbPawns[FLIP(uColor)] &
BBFILE[uPawnFile]);
while(IS_ON_BOARD(cSquare =
CoorFromBitBoardRank8ToRank1(&bb)))
@@ -2075,12 +2078,12 @@ Return value:
uPawnFile = FILE(c) + 1;
if (pHash->uCountPerFile[uColor][uPawnFile + 1] > 0)
{
- bb = (pHash->bbPawnLocations[uColor] &
+ bb = (pos->bbPawns[uColor] &
BBFILE[uPawnFile] &
BBADJACENT_RANKS[RANK(c)]);
if (!bb)
{
- bb = (pHash->bbPawnLocations[FLIP(uColor)] &
+ bb = (pos->bbPawns[FLIP(uColor)] &
BBFILE[uPawnFile]);
while(IS_ON_BOARD(cSquare =
CoorFromBitBoardRank8ToRank1(&bb)))
@@ -2152,7 +2155,7 @@ Return value:
fast_skip:
uPawnFile = FILE(c) + 1;
ASSERT((1 <= uPawnFile) && (uPawnFile <= 8));
- ASSERT(CountBits(pHash->bbPawnLocations[uColor] & BBFILE[FILE(c)])
+ ASSERT(CountBits(pos->bbPawns[uColor] & BBFILE[FILE(c)])
== pHash->uCountPerFile[uColor][uPawnFile]);
//
@@ -2640,17 +2643,17 @@ Return value:
bbPc = ~(pHash->bbStationaryPawns[WHITE] |
pHash->bbStationaryPawns[BLACK]);
bbPc &= bbMask;
- bb = pHash->bbPawnLocations[uColor] & bbPc;
+ bb = pos->bbPawns[uColor] & bbPc;
while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb)))
{
ASSERT(pos->rgSquare[cSquare].pPiece);
ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece));
ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == uColor);
- ASSERT(pHash->bbPawnLocations[uColor] & COOR_TO_BB(cSquare));
+ ASSERT(pos->bbPawns[uColor] & COOR_TO_BB(cSquare));
i += TRANSIENT_PAWN_ON_BISHOP_COLOR[cSquare];
}
- bb = pHash->bbPawnLocations[FLIP(uColor)] & bbPc;
+ bb = pos->bbPawns[FLIP(uColor)] & bbPc;
while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb)))
{
//
@@ -2660,7 +2663,7 @@ Return value:
ASSERT(pos->rgSquare[cSquare].pPiece);
ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece));
ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == FLIP(uColor));
- ASSERT(pHash->bbPawnLocations[FLIP(uColor)] & COOR_TO_BB(cSquare));
+ ASSERT(pos->bbPawns[FLIP(uColor)] & COOR_TO_BB(cSquare));
i +=
(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)].small.uPawn != 0) *
TRANSIENT_PAWN_ON_BISHOP_COLOR[cSquare] / 2;
@@ -2816,7 +2819,7 @@ Return value:
// by a friendly one -- near the enemy king. This is a
// bishop-specific king-tropism/outpost bonus.
//
- bb = pHash->bbPawnLocations[FLIP(uColor)] &
+ bb = pos->bbPawns[FLIP(uColor)] &
(~pHash->bbStationaryPawns[FLIP(uColor)]);
if (TRUE == _IsSquareSafeFromEnemyPawn(pos, c, bb))
{
@@ -2956,7 +2959,7 @@ Return value:
//
uDist = DISTANCE(c, pos->cNonPawns[FLIP(uColor)][0]);
ASSERT((uDist > 0) && (uDist <= 8));
- bb = pHash->bbPawnLocations[FLIP(uColor)] &
+ bb = pos->bbPawns[FLIP(uColor)] &
(~pHash->bbStationaryPawns[FLIP(uColor)]);
if (TRUE == _IsSquareSafeFromEnemyPawn(pos, c, bb))
{
@@ -4013,7 +4016,7 @@ Return value:
(pHash->uCountPerFile[BLACK][u] > 0);
ASSERT((v >= 0) && (v <= 2));
uCounter += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8))));
- bb = pHash->bbPawnLocations[ufColor] & BBFILE[u - 1];
+ bb = pos->bbPawns[ufColor] & BBFILE[u - 1];
if (bb)
{
if (uColor == WHITE)
@@ -4188,8 +4191,8 @@ Return value:
if ((u == 2) && (uColor == WHITE))
{
i = 0;
- bb = (pHash->bbPawnLocations[WHITE] |
- pHash->bbPawnLocations[BLACK]);
+ bb = (pos->bbPawns[WHITE] |
+ pos->bbPawns[BLACK]);
while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb)))
{
i += (DISTANCE(cSquare, pos->cNonPawns[BLACK][0]) -
@@ -4847,7 +4850,8 @@ Return value:
// save some time. BEFORE ANY CODE BELOW TOUCHES THE ATTACK
// TABLES IT NEEDS TO CLEAR/POPULATE THEM THOUGH!!!
//
- pHash = _EvalPawns(ctx, &fDeferred);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalPawns,
+ pHash = _EvalPawns(ctx, &fDeferred));
ASSERT(NULL != pHash);
ASSERT(IS_VALID_FLAG(fDeferred));
pos->iScore[WHITE] += pHash->iScore[WHITE];
@@ -4948,6 +4952,10 @@ Return value:
{
*piPositional = iAlphaMargin;
}
+#ifdef EVAL_TIME
+ ctx->sCounters.tree.u64CyclesEvalPreLazy +=
+ (SystemReadTimeStampCounter() - uTimer);
+#endif
goto end;
}
else if (iScoreForSideToMove - iBetaMargin >= iBeta)
@@ -4957,6 +4965,10 @@ Return value:
{
*piPositional = iBetaMargin;
}
+#ifdef EVAL_TIME
+ ctx->sCounters.tree.u64CyclesEvalPreLazy +=
+ (SystemReadTimeStampCounter() - uTimer);
+#endif
goto end;
}
}
@@ -4978,13 +4990,19 @@ Return value:
}
#endif
+#ifdef EVAL_TIME
+ ctx->sCounters.tree.u64CyclesEvalPreLazy +=
+ (SystemReadTimeStampCounter() - uTimer);
+#endif
+
//
// If we have to clear/populate the attack table, do it now that
// we know we aren't taking a lazy exit.
//
if (TRUE == fDeferred)
{
- _PopulatePawnAttackBits(pos);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalAttackTablePop,
+ _PopulatePawnAttackBits(pos));
}
//
@@ -5047,11 +5065,13 @@ Return value:
//
if (IS_KNIGHT(p))
{
- _EvalKnight(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalKnight,
+ _EvalKnight(pos, c, pHash));
}
else
{
- _EvalBishop(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop,
+ _EvalBishop(pos, c, pHash));
}
#ifdef EVAL_DUMP
Trace("After %s:\n%d\t\t%d\n", PieceAbbrev(p),
@@ -5088,11 +5108,13 @@ Return value:
//
if (IS_KNIGHT(p))
{
- _EvalKnight(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalKnight,
+ _EvalKnight(pos, c, pHash));
}
else
{
- _EvalBishop(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop,
+ _EvalBishop(pos, c, pHash));
}
#ifdef EVAL_DUMP
Trace("After %s:\n%d\t\t%d\n", PieceAbbrev(p),
@@ -5118,7 +5140,7 @@ Return value:
ASSERT(IS_ON_BOARD(c));
ASSERT(IS_ROOK(pos->rgSquare[c].pPiece));
ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == uColor);
- _EvalRook(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalRook, _EvalRook(pos, c, pHash));
#ifdef EVAL_DUMP
p = BLACK_ROOK | uColor;
Trace("After %s:\n%d\t\t%d\n", PieceAbbrev(p),
@@ -5135,7 +5157,7 @@ Return value:
ASSERT(IS_ON_BOARD(c));
ASSERT(IS_ROOK(pos->rgSquare[c].pPiece));
ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == uColor);
- _EvalRook(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalRook, _EvalRook(pos, c, pHash));
#ifdef EVAL_DUMP
p = BLACK_ROOK | uColor;
Trace("After %s:\n%d\t\t%d\n", PieceAbbrev(p),
@@ -5155,7 +5177,7 @@ Return value:
ASSERT(IS_ON_BOARD(c));
ASSERT(IS_QUEEN(pos->rgSquare[c].pPiece));
ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == uColor);
- _EvalQueen(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalQueen, _EvalQueen(pos, c, pHash));
#ifdef EVAL_DUMP
p = BLACK_ROOK | uColor;
Trace("After %s:\n%d\t\t%d\n", PieceAbbrev(p),
@@ -5172,7 +5194,7 @@ Return value:
ASSERT(IS_ON_BOARD(c));
ASSERT(IS_QUEEN(pos->rgSquare[c].pPiece));
ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == uColor);
- _EvalQueen(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalQueen, _EvalQueen(pos, c, pHash));
#ifdef EVAL_DUMP
p = BLACK_ROOK | uColor;
Trace("After %s:\n%d\t\t%d\n", PieceAbbrev(p),
@@ -5191,7 +5213,7 @@ Return value:
ASSERT(GET_COLOR(p) == BLACK);
ASSERT(IS_KING(p));
#endif
- _EvalKing(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash));
ctx->sPlyInfo[ctx->uPly].iKingScore[BLACK] = pos->iTempScore;
#ifdef EVAL_DUMP
Trace("After *k:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]);
@@ -5205,7 +5227,7 @@ Return value:
ASSERT(GET_COLOR(p) == WHITE);
ASSERT(IS_KING(p));
#endif
- _EvalKing(pos, c, pHash);
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash));
ctx->sPlyInfo[ctx->uPly].iKingScore[WHITE] = pos->iTempScore;
#ifdef EVAL_DUMP
Trace("After .k:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]);
@@ -5216,6 +5238,9 @@ Return value:
// passed pawns identified by the pawn eval routine again. Also
// see if the side not on move has a trapped piece.
//
+#ifdef EVAL_TIME
+ UINT64 u64PostLazyMiscTimer = SystemReadTimeStampCounter();
+#endif
bb = (pHash->bbPasserLocations[WHITE] | pHash->bbPasserLocations[BLACK]);
if (0 != bb)
{
@@ -5281,8 +5306,8 @@ Return value:
// with two pawn wings where having a bishop is an
// advantage.
//
- bb = (pHash->bbPawnLocations[WHITE] |
- pHash->bbPawnLocations[BLACK]);
+ bb = (pos->bbPawns[WHITE] |
+ pos->bbPawns[BLACK]);
ASSERT((BBFILE[A] | BBFILE[B] | BBFILE[C]) ==
0x0707070707070707ULL);
ASSERT((BBFILE[F] | BBFILE[G] | BBFILE[H]) ==
@@ -5334,6 +5359,11 @@ Return value:
(SCORE)REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[WHITE]]) / 8;
pos->iScore[WHITE] += iAlphaMargin;
+#ifdef EVAL_TIME
+ ctx->sCounters.tree.u64CyclesEvalPostLazyMisc +=
+ (SystemReadTimeStampCounter() - u64PostLazyMiscTimer);
+#endif
+
//
// Almost done
//
diff --git a/src/root.c b/src/root.c
index bfcf034..5ce4c9e 100755
--- a/src/root.c
+++ b/src/root.c
@@ -563,6 +563,58 @@ Return value:
#ifdef EVAL_TIME
n = (double)ctx->sCounters.tree.u64CyclesInEval;
Trace("Avg. cpu cycles in eval: %8.1f.\n", (n / d));
+ {
+ //
+ // Per-term breakdown of the average above -- board_
+ // representation/EVAL.md section 0b. "other" covers every
+ // term not individually timed (pawn structure detail beyond
+ // _EvalPawns' own bucket, bishop pairs, bad-trade/passer-race
+ // detection, lazy-eval margin estimation, etc.) computed as
+ // the remainder rather than its own counter, so this always
+ // sums exactly to the total above regardless of what future
+ // terms get their own bucket.
+ //
+ UINT64 u64Pawns = ctx->sCounters.tree.u64CyclesEvalPawns;
+ UINT64 u64Knight = ctx->sCounters.tree.u64CyclesEvalKnight;
+ UINT64 u64Bishop = ctx->sCounters.tree.u64CyclesEvalBishop;
+ UINT64 u64Rook = ctx->sCounters.tree.u64CyclesEvalRook;
+ UINT64 u64Queen = ctx->sCounters.tree.u64CyclesEvalQueen;
+ UINT64 u64King = ctx->sCounters.tree.u64CyclesEvalKing;
+ //
+ // u64PreLazy already contains u64Pawns as a subset (pawns
+ // runs nested inside the pre-lazy segment, not after it) --
+ // split it out here rather than double-counting both.
+ //
+ UINT64 u64PreLazy = ctx->sCounters.tree.u64CyclesEvalPreLazy;
+ UINT64 u64PreLazyOther = (u64PreLazy >= u64Pawns) ?
+ (u64PreLazy - u64Pawns) : 0;
+ UINT64 u64PostMisc = ctx->sCounters.tree.u64CyclesEvalPostLazyMisc;
+ UINT64 u64AttackPop = ctx->sCounters.tree.u64CyclesEvalAttackTablePop;
+ UINT64 u64Named = (u64PreLazy + u64Knight + u64Bishop +
+ u64Rook + u64Queen + u64King + u64PostMisc +
+ u64AttackPop);
+ UINT64 u64Total = ctx->sCounters.tree.u64CyclesInEval;
+ UINT64 u64Unaccounted = (u64Total >= u64Named) ?
+ (u64Total - u64Named) : 0;
+
+ Trace(" eval breakdown (%% of total cycles in eval):\n");
+ Trace(" pre-lazy (always paid): %5.1f%% "
+ "(of which pawns: %5.1f%%, other pre-lazy: %5.1f%%)\n",
+ (u64Total ? (100.0 * (double)u64PreLazy / (double)u64Total) : 0.0),
+ (u64Total ? (100.0 * (double)u64Pawns / (double)u64Total) : 0.0),
+ (u64Total ? (100.0 * (double)u64PreLazyOther / (double)u64Total) : 0.0));
+ Trace(" knight: %5.1f%% bishop: %5.1f%% rook: %5.1f%%\n",
+ (u64Total ? (100.0 * (double)u64Knight / (double)u64Total) : 0.0),
+ (u64Total ? (100.0 * (double)u64Bishop / (double)u64Total) : 0.0),
+ (u64Total ? (100.0 * (double)u64Rook / (double)u64Total) : 0.0));
+ Trace(" queen: %5.1f%% king: %5.1f%% post-lazy misc: %5.1f%%\n",
+ (u64Total ? (100.0 * (double)u64Queen / (double)u64Total) : 0.0),
+ (u64Total ? (100.0 * (double)u64King / (double)u64Total) : 0.0),
+ (u64Total ? (100.0 * (double)u64PostMisc / (double)u64Total) : 0.0));
+ Trace(" attack-table pop: %5.1f%% unaccounted: %5.1f%%\n",
+ (u64Total ? (100.0 * (double)u64AttackPop / (double)u64Total) : 0.0),
+ (u64Total ? (100.0 * (double)u64Unaccounted / (double)u64Total) : 0.0));
+ }
#endif
#endif
}