Skip to content

Commit 9400bfd

Browse files
authored
Optimizing quiescence search (#123)
# πŸ“Œ Optimizing quiescence search ## πŸ“„ Description - Splitted legal moves generation into two, adding a legal capture moves generation. This new legal capture moves generation avoids creating all legal moves and then filtering for capture, hence resulting in a non-negligible speedup (see Additional Notes). Quiescence search now use legal capture moves generation and negamax search uses legal moves generation. ## 🧩 Type of Change πŸš€ Performance improvement ## πŸ“ Additional Notes <!-- optional --> ### Benchmark setup A quick benchmark was run on the [Kiwipete](https://grandchesstree.com/perft/1/results) position. Hardware used: M4 MacBook Pro with 16 Gb Unified Memory. ```txt d a b c d e f g h +---+---+---+---+---+---+---+---+ 8 | r | | | | k | | | r | 8 +---+---+---+---+---+---+---+---+ 7 | p | | p | p | q | p | b | | 7 +---+---+---+---+---+---+---+---+ 6 | b | n | | | p | n | p | | 6 +---+---+---+---+---+---+---+---+ 5 | | | | P | N | | | | 5 +---+---+---+---+---+---+---+---+ 4 | | p | | | P | | | | 4 +---+---+---+---+---+---+---+---+ 3 | | | N | | | Q | | p | 3 +---+---+---+---+---+---+---+---+ 2 | P | P | P | B | B | P | P | P | 2 +---+---+---+---+---+---+---+---+ 1 | R | | | | K | | | R | 1 +---+---+---+---+---+---+---+---+ a b c d e f g h FEN notation: r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w - - 0 1 Zobrist hash: 7132688269263558591 ``` ### Benchmark results | Experiment (depth 1) | Total nodes | Negamax nodes | Quiescence nodes | Total time (s) | Nodes per second | | -------------------- | ----------- | ------------- | ---------------- | -------------- | ---------------- | | Before | 34'038'939 | 47 | 34'038'892 | 12.0681 | 2'820'582 | | After | 34'038'939 | 47 | 34'038'892 | 8.07602 | 4'214'818 | | Result | = | = | = | -3.99208 | + 1'394'236 | | Experiment (depth 2) | Total nodes | Negamax nodes | Quiescence nodes | Total time (s) | Nodes per second | | -------------------- | ----------- | ------------- | ---------------- | -------------- | ---------------- | | Before | 80'460'423 | 610 | 80'459'813 | 28.2208 | 2'851'100 | | After | 80'460'423 | 610 | 80'459'813 | 18.656 | 4'312'841 | | Result | = | = | = | -9.5648 | + 1'461'741 | Still, most of the nodes explored here are from the quiescence search. This number could be drastically reduced using for instance [Static Exchange Evaluation](https://www.chessprogramming.org/Static_Exchange_Evaluation).
1 parent 90d31c0 commit 9400bfd

9 files changed

Lines changed: 172 additions & 44 deletions

File tree

β€Žinclude/bitbishop/movegen/bishop_moves.hppβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
* computed for the current position.
4646
*/
4747
inline void generate_bishop_legal_moves(std::vector<Move>& moves, const Board& board, Color us,
48-
const Bitboard& check_mask, const PinResult& pins) {
48+
const Bitboard& check_mask, const PinResult& pins,
49+
const Bitboard& allowed_targets = Bitboard::Ones()) {
4950
const Bitboard own = board.friendly(us);
5051
const Bitboard enemy = board.enemy(us);
5152
const Bitboard occupied = board.occupied();
@@ -58,6 +59,7 @@ inline void generate_bishop_legal_moves(std::vector<Move>& moves, const Board& b
5859
Bitboard candidates = bishop_attacks(from, occupied);
5960
candidates &= ~own;
6061
candidates &= check_mask;
62+
candidates &= allowed_targets;
6163
if (pins.pinned.test(from)) {
6264
candidates &= pins.pin_ray[from.value()];
6365
}

β€Žinclude/bitbishop/movegen/king_moves.hppβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
#include <vector>
99

1010
inline void generate_legal_king_moves(std::vector<Move>& moves, const Board& board, Color us, Square king_sq,
11-
const Bitboard& enemy_attacks) {
11+
const Bitboard& enemy_attacks,
12+
const Bitboard& allowed_targets = Bitboard::Ones()) {
1213
const Bitboard own = board.friendly(us);
1314
const Bitboard enemy = board.enemy(us);
1415

1516
Bitboard candidates = Lookups::KING_ATTACKS[king_sq.value()];
1617

1718
candidates &= ~own;
1819
candidates &= ~enemy_attacks;
20+
candidates &= allowed_targets;
1921

2022
for (Square to : candidates) {
2123
const bool is_capture = enemy.test(to);

β€Žinclude/bitbishop/movegen/knight_moves.hppβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
// pinned knights cannot move at all due to knight's l-shaped move geometry
1212
inline void generate_knight_legal_moves(std::vector<Move>& moves, const Board& board, Color us,
13-
const Bitboard& check_mask, const PinResult& pins) {
13+
const Bitboard& check_mask, const PinResult& pins,
14+
const Bitboard& allowed_targets = Bitboard::Ones()) {
1415
const Bitboard own = board.friendly(us);
1516
const Bitboard enemy = board.enemy(us);
1617
Bitboard knights = board.knights(us);
@@ -23,6 +24,7 @@ inline void generate_knight_legal_moves(std::vector<Move>& moves, const Board& b
2324
Bitboard candidates = Lookups::KNIGHT_ATTACKS[from.value()];
2425
candidates &= ~own;
2526
candidates &= check_mask;
27+
candidates &= allowed_targets;
2628

2729
for (Square to : candidates) {
2830
const bool is_capture = enemy.test(to);

β€Žinclude/bitbishop/movegen/legal_moves.hppβ€Ž

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,16 @@
1818
#include <bitbishop/movegen/rook_moves.hpp>
1919
#include <vector>
2020

21-
/**
22-
* @brief Generates all legal moves for the given side in the current position.
23-
*
24-
* This function computes the full set of legal moves for @p us, taking into
25-
* account checks, pins, enemy attacks, and special rules such as castling
26-
* and en passant. Move legality is enforced during generation rather than
27-
* by post-filtering.
28-
*
29-
* The generation pipeline is:
30-
* - Detect checkers and compute the check resolution mask
31-
* - Compute pinned pieces and their allowed movement rays
32-
* - Generate legal king moves (always allowed)
33-
* - Generate legal castling moves (only when not in check)
34-
* - If in double check, stop after king moves
35-
* - Otherwise, generate legal moves for all remaining pieces
36-
*
37-
* @param moves Vector to append generated legal moves to
38-
* @param board Current board position
39-
*
40-
* @note The move list is appended to; it is not cleared by this function.
41-
* @note Assumes the board position is internally consistent and legal.
42-
*/
43-
inline void generate_legal_moves(std::vector<Move>& moves, const Board& board) {
21+
namespace MoveGen {
22+
enum class Scope {
23+
AllMoves,
24+
CapturesOnly,
25+
};
26+
} // namespace MoveGen
27+
28+
inline void generate_legal_moves_with_scope(std::vector<Move>& moves, const Board& board, MoveGen::Scope scope) {
29+
const bool captures_only = scope == MoveGen::Scope::CapturesOnly;
30+
4431
Color us = board.get_state().m_is_white_turn ? Color::WHITE : Color::BLACK;
4532
Color them = ColorUtil::opposite(us);
4633

@@ -50,18 +37,39 @@ inline void generate_legal_moves(std::vector<Move>& moves, const Board& board) {
5037
Bitboard check_mask = compute_check_mask(king_sq, checkers, board);
5138
PinResult pins = compute_pins(king_sq, board, us);
5239
Bitboard enemy_attacks = generate_attacks(board, them);
40+
Bitboard enemy = board.enemy(us);
41+
Bitboard allowed_targets = captures_only ? enemy : Bitboard::Ones();
5342

54-
generate_legal_king_moves(moves, board, us, king_sq, enemy_attacks);
43+
generate_legal_king_moves(moves, board, us, king_sq, enemy_attacks, allowed_targets);
5544

56-
generate_castling_moves(moves, board, us, checkers, enemy_attacks);
45+
if (!captures_only) {
46+
generate_castling_moves(moves, board, us, checkers, enemy_attacks);
47+
}
5748

5849
if (checkers.count() > 1) {
5950
return;
6051
}
6152

62-
generate_knight_legal_moves(moves, board, us, check_mask, pins);
63-
generate_bishop_legal_moves(moves, board, us, check_mask, pins);
64-
generate_rook_legal_moves(moves, board, us, check_mask, pins);
65-
generate_queen_legal_moves(moves, board, us, check_mask, pins);
66-
generate_pawn_legal_moves(moves, board, us, king_sq, check_mask, pins);
53+
generate_knight_legal_moves(moves, board, us, check_mask, pins, allowed_targets);
54+
generate_bishop_legal_moves(moves, board, us, check_mask, pins, allowed_targets);
55+
generate_rook_legal_moves(moves, board, us, check_mask, pins, allowed_targets);
56+
generate_queen_legal_moves(moves, board, us, check_mask, pins, allowed_targets);
57+
generate_pawn_legal_moves(moves, board, us, king_sq, check_mask, pins, captures_only);
58+
}
59+
60+
/**
61+
* @brief Generates all legal moves for the side to move.
62+
*/
63+
inline void generate_legal_moves(std::vector<Move>& moves, const Board& board) {
64+
generate_legal_moves_with_scope(moves, board, MoveGen::Scope::AllMoves);
65+
}
66+
67+
/**
68+
* @brief Generates only legal capture moves for the side to move.
69+
*
70+
* Includes king captures, piece captures, pawn captures and legal en passant.
71+
* Excludes non-capture moves (quiet king moves, pawn pushes, castling, ...).
72+
*/
73+
inline void generate_legal_capture_moves(std::vector<Move>& moves, const Board& board) {
74+
generate_legal_moves_with_scope(moves, board, MoveGen::Scope::CapturesOnly);
6775
}

β€Žinclude/bitbishop/movegen/pawn_moves.hppβ€Ž

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,8 @@ inline void generate_en_passant(std::vector<Move>& moves, Square from, Color us,
254254
* @param pins Pin result structure indicating which pieces are pinned
255255
*/
256256
inline void generate_pawn_legal_moves(std::vector<Move>& moves, const Board& board, Color us, Square king_sq,
257-
const Bitboard& check_mask, const PinResult& pins) {
257+
const Bitboard& check_mask, const PinResult& pins,
258+
bool captures_only = false) {
258259
const Bitboard enemy = board.enemy(us);
259260
const Bitboard occupied = board.occupied();
260261
Bitboard pawns = board.pawns(us);
@@ -264,8 +265,10 @@ inline void generate_pawn_legal_moves(std::vector<Move>& moves, const Board& boa
264265
const bool is_pinned = pins.pinned.test(from);
265266
const Bitboard pin_mask = is_pinned ? pins.pin_ray[from.flat_index()] : Bitboard::Ones();
266267

267-
generate_single_push(moves, from, us, occupied, check_mask, pin_mask);
268-
generate_double_push(moves, from, us, occupied, check_mask, pin_mask);
268+
if (!captures_only) {
269+
generate_single_push(moves, from, us, occupied, check_mask, pin_mask);
270+
generate_double_push(moves, from, us, occupied, check_mask, pin_mask);
271+
}
269272
generate_captures(moves, from, us, enemy, check_mask, pin_mask);
270273
generate_en_passant(moves, from, us, board, king_sq, check_mask, pin_mask);
271274
}

β€Žinclude/bitbishop/movegen/queen_moves.hppβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
* - Promotions, en passant, and castling are not applicable to queen moves.
4747
*/
4848
inline void generate_queen_legal_moves(std::vector<Move>& moves, const Board& board, Color us,
49-
const Bitboard& check_mask, const PinResult& pins) {
49+
const Bitboard& check_mask, const PinResult& pins,
50+
const Bitboard& allowed_targets = Bitboard::Ones()) {
5051
const Bitboard own = board.friendly(us);
5152
const Bitboard enemy = board.enemy(us);
5253
const Bitboard occupied = board.occupied();
@@ -59,6 +60,7 @@ inline void generate_queen_legal_moves(std::vector<Move>& moves, const Board& bo
5960
Bitboard candidates = queen_attacks(from, occupied);
6061
candidates &= ~own;
6162
candidates &= check_mask;
63+
candidates &= allowed_targets;
6264
if (pins.pinned.test(from)) {
6365
candidates &= pins.pin_ray[from.value()];
6466
}

β€Žinclude/bitbishop/movegen/rook_moves.hppβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
* - Promotions, en passant, and castling are not applicable to rook moves.
4747
*/
4848
inline void generate_rook_legal_moves(std::vector<Move>& moves, const Board& board, Color us,
49-
const Bitboard& check_mask, const PinResult& pins) {
49+
const Bitboard& check_mask, const PinResult& pins,
50+
const Bitboard& allowed_targets = Bitboard::Ones()) {
5051
const Bitboard own = board.friendly(us);
5152
const Bitboard enemy = board.enemy(us);
5253
const Bitboard occupied = board.occupied();
@@ -59,6 +60,7 @@ inline void generate_rook_legal_moves(std::vector<Move>& moves, const Board& boa
5960
Bitboard candidates = rook_attacks(from, occupied);
6061
candidates &= ~own;
6162
candidates &= check_mask;
63+
candidates &= allowed_targets;
6264
if (pins.pinned.test(from)) {
6365
candidates &= pins.pin_ray[from.value()];
6466
}

β€Žsrc/bitbishop/engine/search.cppβ€Ž

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,12 @@ int Search::quiesce(Position& position, int alpha, int beta, SearchStats& stats,
3434
}
3535

3636
std::vector<Move> moves;
37-
generate_legal_moves(moves, board);
38-
// Optimization: implement generate_capture_moves(moves, board); instead
39-
// To generate only capture moves and not all moves top discard some in the end
37+
generate_legal_capture_moves(moves, board);
4038

4139
for (const Move& move : moves) {
4240
if (stop_flag != nullptr && stop_flag->load()) {
4341
return alpha;
4442
}
45-
if (!move.is_capture) {
46-
continue;
47-
}
4843
position.apply_move(move);
4944

5045
// Quiescence window flip: child is searched with (-beta, -alpha) and the returned score is negated.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <bitbishop/board.hpp>
4+
#include <bitbishop/helpers/moves.hpp>
5+
#include <bitbishop/move.hpp>
6+
#include <bitbishop/movegen/legal_moves.hpp>
7+
#include <bitbishop/square.hpp>
8+
9+
using namespace Squares;
10+
using namespace Pieces;
11+
12+
TEST(GenerateLegalCaptureMovesTest, StartingPositionHasNoCaptures) {
13+
Board board = Board::StartingPosition();
14+
15+
BoardState state = board.get_state();
16+
state.m_is_white_turn = true;
17+
board.set_state(state);
18+
19+
std::vector<Move> moves;
20+
generate_legal_capture_moves(moves, board);
21+
22+
EXPECT_TRUE(moves.empty());
23+
}
24+
25+
TEST(GenerateLegalCaptureMovesTest, ExcludesQuietMovesAndCastling) {
26+
Board board("r3k2r/8/8/8/r7/8/8/R3K2R w KQkq - 0 1");
27+
28+
std::vector<Move> moves;
29+
generate_legal_capture_moves(moves, board);
30+
31+
EXPECT_GT(moves.size(), 0);
32+
EXPECT_TRUE(contains_move(moves, {A1, A4, std::nullopt, true, false, false}));
33+
EXPECT_TRUE(contains_move(moves, {H1, H8, std::nullopt, true, false, false}));
34+
35+
EXPECT_FALSE(contains_move(moves, {E1, G1, std::nullopt, false, false, true}));
36+
EXPECT_FALSE(contains_move(moves, {E1, C1, std::nullopt, false, false, true}));
37+
EXPECT_FALSE(contains_move(moves, {A1, A2, std::nullopt, false, false, false}));
38+
39+
for (const Move& move : moves) {
40+
EXPECT_TRUE(move.is_capture);
41+
}
42+
}
43+
44+
TEST(GenerateLegalCaptureMovesTest, IncludesEnPassantCapture) {
45+
Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1");
46+
47+
std::vector<Move> moves;
48+
generate_legal_capture_moves(moves, board);
49+
50+
EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false}));
51+
EXPECT_EQ(count_en_passant(moves), 1);
52+
for (const Move& move : moves) {
53+
EXPECT_TRUE(move.is_capture);
54+
}
55+
}
56+
57+
TEST(GenerateLegalCaptureMovesTest, IncludesCapturePromotionsOnly) {
58+
Board board = Board::Empty();
59+
board.set_piece(E1, WHITE_KING);
60+
board.set_piece(A8, BLACK_KING);
61+
board.set_piece(G7, WHITE_PAWN);
62+
board.set_piece(H8, BLACK_ROOK);
63+
64+
BoardState state = board.get_state();
65+
state.m_is_white_turn = true;
66+
board.set_state(state);
67+
68+
std::vector<Move> moves;
69+
generate_legal_capture_moves(moves, board);
70+
71+
EXPECT_EQ(moves.size(), 4);
72+
EXPECT_TRUE(contains_move(moves, {G7, H8, WHITE_QUEEN, true, false, false}));
73+
EXPECT_TRUE(contains_move(moves, {G7, H8, WHITE_ROOK, true, false, false}));
74+
EXPECT_TRUE(contains_move(moves, {G7, H8, WHITE_BISHOP, true, false, false}));
75+
EXPECT_TRUE(contains_move(moves, {G7, H8, WHITE_KNIGHT, true, false, false}));
76+
77+
EXPECT_FALSE(contains_move(moves, {G7, G8, WHITE_QUEEN, false, false, false}));
78+
EXPECT_FALSE(contains_move(moves, {G7, G8, WHITE_ROOK, false, false, false}));
79+
EXPECT_FALSE(contains_move(moves, {G7, G8, WHITE_BISHOP, false, false, false}));
80+
EXPECT_FALSE(contains_move(moves, {G7, G8, WHITE_KNIGHT, false, false, false}));
81+
}
82+
83+
TEST(GenerateLegalCaptureMovesTest, InCheckOnlyLegalCaptureResponses) {
84+
Board board = Board::Empty();
85+
board.set_piece(E1, WHITE_KING);
86+
board.set_piece(B5, WHITE_BISHOP);
87+
board.set_piece(E8, BLACK_ROOK);
88+
board.set_piece(H8, BLACK_KING);
89+
board.set_piece(A6, BLACK_PAWN);
90+
91+
BoardState state = board.get_state();
92+
state.m_is_white_turn = true;
93+
board.set_state(state);
94+
95+
std::vector<Move> moves;
96+
generate_legal_capture_moves(moves, board);
97+
98+
EXPECT_EQ(moves.size(), 1);
99+
EXPECT_TRUE(contains_move(moves, {B5, E8, std::nullopt, true, false, false}));
100+
EXPECT_FALSE(contains_move(moves, {B5, A6, std::nullopt, true, false, false}));
101+
}
102+
103+
TEST(GenerateLegalCaptureMovesTest, MovesVectorIsAppendedNotCleared) {
104+
Board board = Board::StartingPosition();
105+
std::vector<Move> moves;
106+
moves.emplace_back(Move::make(A1, A2));
107+
108+
generate_legal_capture_moves(moves, board);
109+
110+
EXPECT_EQ(moves.size(), 1);
111+
EXPECT_TRUE(contains_move(moves, Move::make(A1, A2)));
112+
}

0 commit comments

Comments
Β (0)