Speed up move generation and position updates (bench 5725647)

Co-authored-by: GPT 5.6 Sol <codex@openai.com>
This commit is contained in:
2026-07-09 22:03:15 +02:00
parent c010a37702
commit d6a3386753
3 changed files with 67 additions and 60 deletions

View File

@@ -14,7 +14,7 @@
## Implements low-level bit operations
import std/[sugar, bitops, strutils]
import std/[bitops, strutils]
import heimdall/[moves, pieces]
@@ -147,31 +147,30 @@ func pretty*(self: Bitboard): string =
func `$`*(self: Bitboard): string {.inline.} = self.pretty()
func generateShifters: array[White..Black, array[Direction, (Bitboard {.noSideEffect.} -> Bitboard)]] {.compileTime.} =
result[White][Forward] = (x: Bitboard) => x shr 8
result[White][Backward] = (x: Bitboard) => x shl 8
result[White][Left] = (x: Bitboard) => x shr 1
result[White][Right] = (x: Bitboard) => x shl 1
result[White][ForwardRight] = (x: Bitboard) => x shr 7
result[White][ForwardLeft] = (x: Bitboard) => x shr 9
result[White][BackwardRight] = (x: Bitboard) => x shl 9
result[White][BackwardLeft] = (x: Bitboard) => x shl 7
result[Black][Backward] = (x: Bitboard) => x shr 8
result[Black][Forward] = (x: Bitboard) => x shl 8
result[Black][Right] = (x: Bitboard) => x shr 1
result[Black][Left] = (x: Bitboard) => x shl 1
result[Black][BackwardLeft] = (x: Bitboard) => x shr 7
result[Black][BackwardRight] = (x: Bitboard) => x shr 9
result[Black][ForwardLeft] = (x: Bitboard) => x shl 9
result[Black][ForwardRight] = (x: Bitboard) => x shl 7
const shifters: array[White..Black, array[Direction, (Bitboard) {.noSideEffect.} -> Bitboard]] = generateShifters()
func directionMask*(bitboard: Bitboard, color: PieceColor, direction: Direction): Bitboard {.inline.} =
shifters[color][direction](bitboard)
case color:
of White:
case direction:
of Forward: result = bitboard shr 8
of Backward: result = bitboard shl 8
of Left: result = bitboard shr 1
of Right: result = bitboard shl 1
of ForwardRight: result = bitboard shr 7
of ForwardLeft: result = bitboard shr 9
of BackwardRight: result = bitboard shl 9
of BackwardLeft: result = bitboard shl 7
of Black:
case direction:
of Forward: result = bitboard shl 8
of Backward: result = bitboard shr 8
of Left: result = bitboard shl 1
of Right: result = bitboard shr 1
of ForwardRight: result = bitboard shl 7
of ForwardLeft: result = bitboard shl 9
of BackwardRight: result = bitboard shr 9
of BackwardLeft: result = bitboard shr 7
of None:
result = Bitboard(0)
func directionMask*(square: Square, color: PieceColor, direction: Direction): Bitboard {.inline.} =
directionMask(square.toBitboard(), color, direction)

View File

@@ -38,6 +38,9 @@ proc generatePawnMoves(self: var Position, moves: var MoveList, destinationMask:
promotionRank = sideToMove.eighthRank()
startingRank = sideToMove.secondRank()
friendlyKing = self.kingSquare(sideToMove)
backwardOffset = if sideToMove == White: 8 else: -8
backwardLeftOffset = if sideToMove == White: 7 else: -7
backwardRightOffset = if sideToMove == White: 9 else: -9
# If a pawn is pinned diagonally, it cannot push forward
let
@@ -55,14 +58,14 @@ proc generatePawnMoves(self: var Position, moves: var MoveList, destinationMask:
canDoublePush = canDoublePush.forward(sideToMove) and not occupancy and destinationMask
for pawn in singlePushes and not promotionRank:
moves.add(createMove(pawn.toBitboard().backward(sideToMove), pawn))
moves.add(createMove(Square(pawn.int + backwardOffset), pawn))
for pawn in singlePushes and promotionRank:
for promotion in [PromotionBishop, PromotionKnight, PromotionRook, PromotionQueen]:
moves.add(createMove(pawn.toBitboard().backward(sideToMove), pawn, promotion))
moves.add(createMove(Square(pawn.int + backwardOffset), pawn, promotion))
for pawn in canDoublePush:
moves.add(createMove(pawn.toBitboard().doubleBackward(sideToMove), pawn, DoublePush))
moves.add(createMove(Square(pawn.int + backwardOffset * 2), pawn, DoublePush))
let
canCapture = pawns and not orthogonalPins
@@ -70,18 +73,18 @@ proc generatePawnMoves(self: var Position, moves: var MoveList, destinationMask:
canCaptureRightUnpinned = (canCapture and not diagonalPins).forwardRight(sideToMove) and enemyPieces and destinationMask
for pawn in canCaptureRightUnpinned and not promotionRank:
moves.add(createMove(pawn.toBitboard().backwardLeft(sideToMove), pawn, Capture))
moves.add(createMove(Square(pawn.int + backwardLeftOffset), pawn, Capture))
for pawn in canCaptureRightUnpinned and promotionRank:
for promotion in [CapturePromotionBishop, CapturePromotionKnight, CapturePromotionRook, CapturePromotionQueen]:
moves.add(createMove(pawn.toBitboard().backwardLeft(sideToMove), pawn, promotion))
moves.add(createMove(Square(pawn.int + backwardLeftOffset), pawn, promotion))
for pawn in canCaptureLeftUnpinned and not promotionRank:
moves.add(createMove(pawn.toBitboard().backwardRight(sideToMove), pawn, Capture))
moves.add(createMove(Square(pawn.int + backwardRightOffset), pawn, Capture))
for pawn in canCaptureLeftUnpinned and promotionRank:
for promotion in [CapturePromotionBishop, CapturePromotionKnight, CapturePromotionRook, CapturePromotionQueen]:
moves.add(createMove(pawn.toBitboard().backwardRight(sideToMove), pawn, promotion))
moves.add(createMove(Square(pawn.int + backwardRightOffset), pawn, promotion))
# Special cases for pawns pinned diagonally that can capture their pinners
@@ -89,21 +92,21 @@ proc generatePawnMoves(self: var Position, moves: var MoveList, destinationMask:
canCaptureLeft = canCapture.forwardLeft(sideToMove) and enemyPieces and destinationMask
canCaptureRight = canCapture.forwardRight(sideToMove) and enemyPieces and destinationMask
leftPinnedCanCapture = (canCaptureLeft and diagonalPins) and not canCaptureLeftUnpinned
rightPinnedCanCapture = ((canCaptureRight and diagonalPins) and not canCaptureRightUnpinned) and not canCaptureRightUnpinned
rightPinnedCanCapture = (canCaptureRight and diagonalPins) and not canCaptureRightUnpinned
for pawn in leftPinnedCanCapture and not promotionRank:
moves.add(createMove(pawn.toBitboard().backwardRight(sideToMove), pawn, Capture))
moves.add(createMove(Square(pawn.int + backwardRightOffset), pawn, Capture))
for pawn in leftPinnedCanCapture and promotionRank:
for promotion in [CapturePromotionBishop, CapturePromotionKnight, CapturePromotionRook, CapturePromotionQueen]:
moves.add(createMove(pawn.toBitboard().backwardRight(sideToMove), pawn, promotion))
moves.add(createMove(Square(pawn.int + backwardRightOffset), pawn, promotion))
for pawn in rightPinnedCanCapture and not promotionRank:
moves.add(createMove(pawn.toBitboard().backwardLeft(sideToMove), pawn, Capture))
moves.add(createMove(Square(pawn.int + backwardLeftOffset), pawn, Capture))
for pawn in rightPinnedCanCapture and promotionRank:
for promotion in [CapturePromotionBishop, CapturePromotionKnight, CapturePromotionRook, CapturePromotionQueen]:
moves.add(createMove(pawn.toBitboard().backwardLeft(sideToMove), pawn, promotion))
moves.add(createMove(Square(pawn.int + backwardLeftOffset), pawn, promotion))
let epLegality = self.isEPLegal(friendlyKing, epTarget, occupancy, pawns, sideToMove)
if epLegality.left != nullSquare():
@@ -200,8 +203,11 @@ proc generateKnightMoves(self: Position, moves: var MoveList, destinationMask: B
proc generateCastling(self: Position, moves: var MoveList) =
let sideToMove = self.sideToMove
let availability = self.castlingAvailability[sideToMove]
if availability.king == nullSquare() and availability.queen == nullSquare():
return
let
sideToMove = self.sideToMove
castlingRights = self.canCastle()
kingSquare = self.kingSquare(sideToMove)
if castlingRights.king != nullSquare():
@@ -279,7 +285,12 @@ proc doMove*(self: Chessboard, move: Move) {.gcsafe.} =
kingSq = self.position.kingSquare(sideToMove)
king = self.on(kingSq)
self.positions.add(self.position.clone())
# Position is a POD value. Grow the stack first and copy the previous state
# directly into its final slot, avoiding clone()'s zeroed temporary and the
# subsequent second copy performed by seq.add.
let previousPosition = self.positions.high()
self.positions.setLen(self.positions.len() + 1)
copyMem(addr self.positions[^1], addr self.positions[previousPosition], sizeof(Position))
if piece.kind == Pawn or move.isCapture():
self.positions[^1].halfMoveClock = 0
@@ -396,7 +407,9 @@ proc makeNullMove*(self: Chessboard) {.inline.} =
## to the opponent without making a move. This
## is obviously illegal and only to be used during
## search. The move can be undone via unmakeMove
self.positions.add(self.position.clone())
let previousPosition = self.positions.high()
self.positions.setLen(self.positions.len() + 1)
copyMem(addr self.positions[^1], addr self.positions[previousPosition], sizeof(Position))
self.positions[^1].sideToMove = self.position.sideToMove.opposite()
let previousEPTarget = self.positions[^2].enPassantSquare
if previousEPTarget != nullSquare():

View File

@@ -136,8 +136,8 @@ proc slidingAttackers*(self: Position, square: Square, attackingSide: PieceColor
rooks = self.pieces(Rook, attackingSide) or queens
bishops = self.pieces(Bishop, attackingSide) or queens
result = bishopMoves(square, occupancy) and (bishops or queens)
result = result or rookMoves(square, occupancy) and (rooks or queens)
result = bishopMoves(square, occupancy) and bishops
result = result or rookMoves(square, occupancy) and rooks
proc attackers*(self: Position, square: Square, attackingSide: PieceColor): Bitboard {.inline.} =
@@ -438,25 +438,20 @@ proc updateChecksAndPins*(self: var Position) {.inline.} =
if (pinningRay and friendlyPieces).count() == 1:
self.orthogonalPins = self.orthogonalPins or pinningRay
self.threats = Bitboard(0)
let occupancy = friendlyPieces or enemyPieces
for square in enemyPieces:
let piece = self.on(square)
case piece.kind:
of Pawn:
self.threats = self.threats or pawnAttacks(nonSideToMove, square)
of Rook:
self.threats = self.threats or rookMoves(square, occupancy)
of Bishop:
self.threats = self.threats or bishopMoves(square, occupancy)
of Knight:
self.threats = self.threats or knightMoves(square)
of King:
self.threats = self.threats or kingMoves(square)
of Queen:
self.threats = self.threats or (bishopMoves(square, occupancy) or rookMoves(square, occupancy))
else:
discard
let enemyPawns = self.pieces(Pawn, nonSideToMove)
# Pawn attacks can be generated for the whole set at once. Iterating them
# individually adds mailbox lookups and a case dispatch for the most common
# enemy piece without changing the resulting attack map.
self.threats = enemyPawns.forwardLeft(nonSideToMove) or enemyPawns.forwardRight(nonSideToMove)
let enemyQueens = self.pieces(Queen, nonSideToMove)
for square in self.pieces(Rook, nonSideToMove) or enemyQueens:
self.threats = self.threats or rookMoves(square, occupancy)
for square in self.pieces(Bishop, nonSideToMove) or enemyQueens:
self.threats = self.threats or bishopMoves(square, occupancy)
for square in self.pieces(Knight, nonSideToMove):
self.threats = self.threats or knightMoves(square)
self.threats = self.threats or kingMoves(self.kingSquare(nonSideToMove))
proc hash*(self: var Position) =