From 6e8645015f73a09f57e592fbf911e66db845b327 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 5 Sep 2026 01:03:28 -0700 Subject: Replace Eval()'s piece-dispatch loop with per-type bitboard walks The old loop walked cNonPawns[color][1..N] (a flat, arbitrarily- ordered list mixing knight/bishop/rook/queen), doing a mailbox lookup plus a p&0x4/IS_KNIGHT branch per piece to decide which _Eval* function to call, then stashing rooks/queens into cDefer/uDefer arrays to evaluate in a later pass -- a hard-to-predict branch per piece on top of a mailbox read the callee already redoes for its own ASSERT. pos->bbPieces[color][PIECE_TYPE] already exists (incrementally maintained by move.c) and was simply unused by Eval() until now. Replaced the whole dispatch with a direct per-type bitboard walk: knights, then bishops, for side-to-move, then the same for the other side, then rooks both colors, then queens both colors -- same phase order as before (load-bearing for bvAttacks accumulation), just sourced from a bitboard instead of a mixed list + runtime type dispatch. Retires the cDefer/uDefer bookkeeping entirely -- with each type its own direct walk, "rooks after minors" is just "do that walk after this one," nothing to defer. Verified genuinely behavior-neutral, not just crash-free: built the pre-change and post-change binaries side by side and diffed --batch --command output against tests/ecm_ringers.ep_ at sd10. Node counts, scores, and PVs are byte-for-byte identical across all 11 positions; only wall-clock time and NPS differ, consistently in the new version's favor (~3-5% faster on this suite, e.g. 1088239 nps -> 1143141 nps, 1560092 -> 1616844). Also verified via the normal precommit_check.sh gate. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj --- src/eval.c | 229 +++++++++++++++++++++++++++---------------------------------- 1 file changed, 101 insertions(+), 128 deletions(-) (limited to 'src') diff --git a/src/eval.c b/src/eval.c index 35ee663..7287caf 100755 --- a/src/eval.c +++ b/src/eval.c @@ -4803,14 +4803,11 @@ Return value: **/ { - ULONG uDefer[2][2]; - COOR cDefer[2][2][10]; POSITION *pos = &(ctx->sPosition); SCORE iScoreForSideToMove; SCORE iAlphaMargin, iBetaMargin; PAWN_HASH_ENTRY *pHash; COOR c; - PIECE p; ULONG u; ULONG uColor; BITBOARD bb; @@ -5054,131 +5051,111 @@ Return value: // // Evaluate individual pieces. // + // board_representation/EVAL.md section 0/2: replaces the old + // cNonPawns[color][1..N] walk + per-piece mailbox lookup + p&0x4/ + // IS_KNIGHT branch dispatch (a hard-to-predict branch per piece, + // on top of a redundant mailbox read the callee already redoes + // for its own ASSERT) with a direct per-type location-bitboard + // walk. pos->bbPieces[color][PIECE_TYPE] already exists, + // incrementally maintained by move.c, and was simply unused by + // Eval() until now -- no new infrastructure, purely a consumer + // change. Also retires the cDefer/uDefer "remember the rooks and + // queens for later" bookkeeping entirely: since each type is now + // its own direct bitboard walk, "evaluate rooks after minors" is + // just "do the rook-bitboard walk after the minor-bitboard walks" + // -- nothing to defer. + // + // Phase order preserved exactly as before (side-to-move's minors, + // then the other side's minors, then rooks both colors, then + // queens both colors) -- that ordering is load-bearing for + // bvAttacks accumulation (each piece's mobility/danger depends on + // attack bits already written by earlier-evaluated pieces this + // same Eval() call). Knight-vs-bishop order *within* the same + // color/phase, and encounter order within a single type's own + // bitboard walk, were never meaningful before (cNonPawns' order + // is arbitrary swap-with-last-on-removal, not stable) and stay + // that way -- EVAL_TERM's plain score accumulation doesn't care. + // pos->uMinorsAtHome[BLACK] = pos->uMinorsAtHome[WHITE] = 0; - uDefer[BLACK][0] = uDefer[BLACK][1] = - uDefer[WHITE][0] = uDefer[WHITE][1] = 0; uColor = pos->uToMove; - for (u = 1; // skips the king - u < pos->uNonPawnCount[uColor][0]; - u++) + bb = pos->bbPieces[uColor][KNIGHT]; + while (bb) { - c = pos->cNonPawns[uColor][u]; - ASSERT(IS_ON_BOARD(c)); - p = pos->rgSquare[c].pPiece; - ASSERT(IS_VALID_PIECE(p)); - ASSERT(GET_COLOR(p) == uColor); - ASSERT(!IS_PAWN(p) && !IS_KING(p)); - - if (p & 0x4) - { - ASSERT(IS_BISHOP(p) || IS_KNIGHT(p)); - - // - // Note: The side on move's pieces of value X are - // evaluated and have their mobility counted and added to - // the attack table before the other side's pieces. Also, - // we do not care if this piece is in danger b/c we will - // find out later on in EvalLookForDanger anyway. - // - if (IS_KNIGHT(p)) - { - TIMED_EVAL_CALL(ctx, u64CyclesEvalKnight, - _EvalKnight(pos, c, pHash)); - } - else - { - TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop, - _EvalBishop(pos, c, pHash)); - } + u = FastFirstBit(bb) - 1; + bb &= (bb - 1); + c = BIT_NUMBER_TO_COOR(u); + TIMED_EVAL_CALL(ctx, u64CyclesEvalKnight, + _EvalKnight(pos, c, pHash)); #ifdef EVAL_DUMP - Trace("After %s:\n%d\t\t%d\n", PieceAbbrev(p), - pos->iScore[WHITE], pos->iScore[BLACK]); + Trace("After N:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); +#endif + } + bb = pos->bbPieces[uColor][BISHOP]; + while (bb) + { + u = FastFirstBit(bb) - 1; + bb &= (bb - 1); + c = BIT_NUMBER_TO_COOR(u); + TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop, + _EvalBishop(pos, c, pHash)); +#ifdef EVAL_DUMP + Trace("After B:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif - } - else - { - ASSERT(IS_ROOK(p) || IS_QUEEN(p)); - cDefer[uColor][IS_QUEEN(p)][uDefer[uColor][IS_QUEEN(p)]++] = c; - } } uColor = FLIP(uColor); ASSERT(uColor != pos->uToMove); - for (u = 1; - u < pos->uNonPawnCount[uColor][0]; - u++) + bb = pos->bbPieces[uColor][KNIGHT]; + while (bb) { - c = pos->cNonPawns[uColor][u]; - ASSERT(IS_ON_BOARD(c)); - p = pos->rgSquare[c].pPiece; - ASSERT(IS_VALID_PIECE(p)); - ASSERT(GET_COLOR(p) == uColor); - ASSERT(!IS_PAWN(p) && !IS_KING(p)); - if (p & 0x4) - { - // - // This time we are evaluating a piece from the side not - // on move. We will not go back later and look at this - // piece again so if it's in danger then record it. The - // drawback, of course, is that danger is only detected if - // a piece is attacked by an enemy of lesser value! - // - if (IS_KNIGHT(p)) - { - TIMED_EVAL_CALL(ctx, u64CyclesEvalKnight, - _EvalKnight(pos, c, pHash)); - } - else - { - TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop, - _EvalBishop(pos, c, pHash)); - } + u = FastFirstBit(bb) - 1; + bb &= (bb - 1); + c = BIT_NUMBER_TO_COOR(u); + TIMED_EVAL_CALL(ctx, u64CyclesEvalKnight, + _EvalKnight(pos, c, pHash)); #ifdef EVAL_DUMP - Trace("After %s:\n%d\t\t%d\n", PieceAbbrev(p), - pos->iScore[WHITE], pos->iScore[BLACK]); + Trace("After N:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); +#endif + } + bb = pos->bbPieces[uColor][BISHOP]; + while (bb) + { + u = FastFirstBit(bb) - 1; + bb &= (bb - 1); + c = BIT_NUMBER_TO_COOR(u); + TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop, + _EvalBishop(pos, c, pHash)); +#ifdef EVAL_DUMP + Trace("After B:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif - } - else - { - ASSERT(IS_ROOK(p) || IS_QUEEN(p)); - cDefer[uColor][IS_QUEEN(p)][uDefer[uColor][IS_QUEEN(p)]++] = c; - } } // // Evaluate any rook(s) for side on move then for side not on move. // uColor = FLIP(uColor); - for (u = 0; - u < uDefer[uColor][0]; - u++) + bb = pos->bbPieces[uColor][ROOK]; + while (bb) { - c = cDefer[uColor][0][u]; - ASSERT(IS_ON_BOARD(c)); - ASSERT(IS_ROOK(pos->rgSquare[c].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == uColor); + u = FastFirstBit(bb) - 1; + bb &= (bb - 1); + c = BIT_NUMBER_TO_COOR(u); 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), - pos->iScore[WHITE], pos->iScore[BLACK]); + Trace("After R:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif } uColor = FLIP(uColor); - for (u = 0; - u < uDefer[uColor][0]; - u++) + bb = pos->bbPieces[uColor][ROOK]; + while (bb) { - c = cDefer[uColor][0][u]; - ASSERT(IS_ON_BOARD(c)); - ASSERT(IS_ROOK(pos->rgSquare[c].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == uColor); + u = FastFirstBit(bb) - 1; + bb &= (bb - 1); + c = BIT_NUMBER_TO_COOR(u); 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), - pos->iScore[WHITE], pos->iScore[BLACK]); + Trace("After R:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif } @@ -5186,36 +5163,28 @@ Return value: // Evaluate any queen(s) for side on move then for side not on move. // uColor = FLIP(uColor); - for (u = 0; - u < uDefer[uColor][1]; - u++) + bb = pos->bbPieces[uColor][QUEEN]; + while (bb) { - c = cDefer[uColor][1][u]; - ASSERT(IS_ON_BOARD(c)); - ASSERT(IS_QUEEN(pos->rgSquare[c].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == uColor); + u = FastFirstBit(bb) - 1; + bb &= (bb - 1); + c = BIT_NUMBER_TO_COOR(u); 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), - pos->iScore[WHITE], pos->iScore[BLACK]); + Trace("After Q:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif } uColor = FLIP(uColor); - for (u = 0; - u < uDefer[uColor][1]; - u++) + bb = pos->bbPieces[uColor][QUEEN]; + while (bb) { - c = cDefer[uColor][1][u]; - ASSERT(IS_ON_BOARD(c)); - ASSERT(IS_QUEEN(pos->rgSquare[c].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[c].pPiece) == uColor); + u = FastFirstBit(bb) - 1; + bb &= (bb - 1); + c = BIT_NUMBER_TO_COOR(u); 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), - pos->iScore[WHITE], pos->iScore[BLACK]); + Trace("After Q:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif } @@ -5225,10 +5194,12 @@ Return value: c = pos->cNonPawns[BLACK][0]; #ifdef DEBUG ASSERT(IS_ON_BOARD(c)); - p = pos->rgSquare[c].pPiece; - ASSERT(IS_VALID_PIECE(p)); - ASSERT(GET_COLOR(p) == BLACK); - ASSERT(IS_KING(p)); + { + PIECE pDebugKing = pos->rgSquare[c].pPiece; + ASSERT(IS_VALID_PIECE(pDebugKing)); + ASSERT(GET_COLOR(pDebugKing) == BLACK); + ASSERT(IS_KING(pDebugKing)); + } #endif TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash)); ctx->sPlyInfo[ctx->uPly].iKingScore[BLACK] = pos->iTempScore; @@ -5239,10 +5210,12 @@ Return value: c = pos->cNonPawns[WHITE][0]; #ifdef DEBUG ASSERT(IS_ON_BOARD(c)); - p = pos->rgSquare[c].pPiece; - ASSERT(IS_VALID_PIECE(p)); - ASSERT(GET_COLOR(p) == WHITE); - ASSERT(IS_KING(p)); + { + PIECE pDebugKing = pos->rgSquare[c].pPiece; + ASSERT(IS_VALID_PIECE(pDebugKing)); + ASSERT(GET_COLOR(pDebugKing) == WHITE); + ASSERT(IS_KING(pDebugKing)); + } #endif TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash)); ctx->sPlyInfo[ctx->uPly].iKingScore[WHITE] = pos->iTempScore; -- cgit v1.3