From 77a8f6ccf31846d63452f79e143fbc6dc62ae3a8 Mon Sep 17 00:00:00 2001 From: anematode Date: Mon, 25 May 2026 11:20:24 +0200 Subject: [PATCH] HQ attacks for AVX2 passed STC (https://tests.stockfishchess.org/tests/view/6a1157dc818cacc1db0ac172): LLR: 2.93 (-2.94,2.94) <0.00,2.00> Total: 29792 W: 7759 L: 7465 D: 14568 Ptnml(0-2): 75, 3206, 8033, 3514, 68 Also passed STC after cleanups https://tests.stockfishchess.org/tests/view/6a121beb818cacc1db0ac35e vondele's local test: Result of 100 runs base (./stockfish.master ) = 1136025 +/- 2816 test (./stockfish.patch2 ) = 1171370 +/- 2848 diff = +35346 +/- 3275 speedup = +0.0311 P(speedup > 0) = 1.0000 Basically we just do hyperbola quintessence in parallel. AVX2 doesn't have efficient bit reversal so we only do it for file and bishop attacks, then do rank attacks separately with a lookup table. This LUT is much smaller which is why this seems to be faster than standard magics. closes https://github.com/official-stockfish/Stockfish/pull/6845 No functional change --- src/attacks.cpp | 58 ++++++++++++++++++++++++++++++++-- src/attacks.h | 84 ++++++++++++++++++++++++++++++++++++++++++++++--- src/misc.h | 8 +++++ 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/src/attacks.cpp b/src/attacks.cpp index ae112287b..15100331f 100644 --- a/src/attacks.cpp +++ b/src/attacks.cpp @@ -30,7 +30,11 @@ Bitboard LineBB[SQUARE_NB][SQUARE_NB]; Bitboard BetweenBB[SQUARE_NB][SQUARE_NB]; Bitboard RayPassBB[SQUARE_NB][SQUARE_NB]; +#ifdef USE_DUAL_HYPERBOLA_QUINT +alignas(64) DualMagic DualMagics[SQUARE_NB]; +#else alignas(64) Magic Magics[SQUARE_NB][2]; +#endif } @@ -40,8 +44,7 @@ using MagicMask = uint16_t; using MagicMask = Bitboard; #endif -#ifdef USE_HYPERBOLA_QUINT -static Bitboard line_mask(Square sq, Direction d1, Direction d2) { +[[maybe_unused]] static Bitboard line_mask(Square sq, Direction d1, Direction d2) { Bitboard mask = 0, dest; for (Direction d : {d1, d2}) { @@ -55,6 +58,7 @@ static Bitboard line_mask(Square sq, Direction d1, Direction d2) { return mask; } +#ifdef USE_HYPERBOLA_QUINT static void init_magics(Magic magics[][2]) { for (Square s = SQ_A1; s <= SQ_H8; ++s) { @@ -71,6 +75,48 @@ static void init_magics(Magic magics[][2]) { } } +#elif defined(USE_DUAL_HYPERBOLA_QUINT) + +// Sliding attacks within a rank, indexed by the slider's file and the +// 8-bit rank occupancy, yielding the 8-bit attack set on that rank +constexpr auto RankAttacks = []() { + std::array, FILE_NB> table{}; + for (int file = 0; file < 8; ++file) + for (int occ = 0; occ < 256; ++occ) + { + uint8_t attacks = 0; + for (int f = file + 1; f <= 7; ++f) + { + attacks |= uint8_t(1 << f); + if (occ & (1 << f)) + break; + } + for (int f = file - 1; f >= 0; --f) + { + attacks |= uint8_t(1 << f); + if (occ & (1 << f)) + break; + } + table[file][occ] = attacks; + } + return table; +}(); + +static void init_dual_magics(DualMagic magics[]) { + for (Square s = SQ_A1; s <= SQ_H8; ++s) + { + DualMagic& m = magics[s]; + m.maskFile = line_mask(s, NORTH, SOUTH); + m.maskDiag = line_mask(s, NORTH_EAST, SOUTH_WEST); + m.maskNone = 0; + m.maskAntidiag = line_mask(s, NORTH_WEST, SOUTH_EAST); + m.r = square_bb(s) * 2; + m.rr = square_bb(Square(63 - int(s))) * 2; + m.rankAttacksLookup = RankAttacks[int(file_of(s))].data(); + m.shift = 8 * int(rank_of(s)); + } +} + #else namespace { @@ -181,7 +227,7 @@ constexpr auto BishopTable = []() { init_magics(BISHOP, result.data(), magics, false); return result; }(); - #else + #elif !defined(USE_DUAL_HYPERBOLA_QUINT) && !defined(USE_HYPERBOLA_QUINT) std::array RookTable; std::array BishopTable; #endif @@ -193,6 +239,8 @@ void init() { #ifdef USE_HYPERBOLA_QUINT init_magics(Magics); +#elif defined(USE_DUAL_HYPERBOLA_QUINT) + init_dual_magics(DualMagics); #else init_magics(ROOK, const_cast(RookTable.data()), Magics, true); init_magics(BISHOP, const_cast(BishopTable.data()), Magics, true); @@ -216,10 +264,14 @@ void init() { } } +#ifdef USE_DUAL_HYPERBOLA_QUINT +const DualMagic& dual_magic(Square s) { return DualMagics[s]; } +#else const Magic& magic(Square s, PieceType pt) { assert((pt == BISHOP || pt == ROOK) && is_ok(s)); return Magics[s][pt - BISHOP]; } +#endif Bitboard line_bb(Square s1, Square s2) { assert(is_ok(s1) && is_ok(s2)); diff --git a/src/attacks.h b/src/attacks.h index f13eb6814..f74cbbd9b 100644 --- a/src/attacks.h +++ b/src/attacks.h @@ -31,6 +31,9 @@ #define USE_HYPERBOLA_QUINT #elif defined(__loongarch__) && __loongarch_grlen == 64 #define USE_HYPERBOLA_QUINT +#elif defined(USE_AVX2) && !defined(USE_PEXT) + #include + #define USE_DUAL_HYPERBOLA_QUINT #endif namespace Stockfish::Attacks { @@ -69,6 +72,61 @@ struct Magic { return hyperbola(occupied, mask1) | hyperbola(occupied, mask2); } }; + +const Magic& magic(Square s, PieceType pt); + +#elif defined(USE_DUAL_HYPERBOLA_QUINT) + +struct DualMagic { + // file, diagonal, unused, antidiagonal + Bitboard maskFile, maskDiag, maskNone, maskAntidiag; + // Precomputed 2 * square_bb(sq), 2 * reverse(square_bb(sq)) + Bitboard r, rr; + + const uint8_t* RESTRICT rankAttacksLookup; + // 8 * rank_of(sq) + int shift; + + // We always compute [bishop, rook] attacks at once, then rely on + // compiler's DCE and CSE to eliminate unneeded re-computations or extractions. + // + // When using hyperbola quintessence, file, diagonal and antidiagonal attacks + // can use a byte reversal rather than a full bit reversal (because all squares + // reside in different bytes). Rank atttacks cannot. Thus, for rank attacks + // only, we use a compact lookup table indexed by the 8 bits of the rank's occupancy. + std::pair both_attacks_bb(Bitboard occupied) const { + // Byteswap within 64-bit elements + const auto bswap = [](__m256i v) { + return _mm256_shuffle_epi8(v, _mm256_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15)); + }; + + // Each lane contains a mask and we follow the same HQ algorithm as + // given above in the ARM64 code path + const __m256i mask = _mm256_load_si256(reinterpret_cast(this)); + const __m256i rs = _mm256_set1_epi64x(r); + const __m256i rrs = _mm256_set1_epi64x(rr); + + __m256i o = _mm256_and_si256(mask, _mm256_set1_epi64x(occupied)); + __m256i fwd = _mm256_sub_epi64(o, rs); + __m256i rev = bswap(_mm256_sub_epi64(bswap(o), rrs)); + __m256i result = _mm256_and_si256(_mm256_xor_si256(fwd, rev), mask); + + // Lane 0: rook attacks (file only); lane 1: bishop attacks + __m128i rookBishop = + _mm_or_si128(_mm256_extracti128_si256(result, 1), _mm256_castsi256_si128(result)); + + Bitboard rowOccupancy = rankAttacksLookup[(occupied >> shift) & 0xff]; + Bitboard rankAttacks = rowOccupancy << shift; + + // [bishop, rook] + return {_mm_extract_epi64(rookBishop, 1), _mm_cvtsi128_si64(rookBishop) + rankAttacks}; + } +}; + +const DualMagic& dual_magic(Square s); + #else // Magic holds all magic bitboards relevant data for a single square struct Magic { @@ -105,12 +163,14 @@ struct Magic { #endif } }; -#endif const Magic& magic(Square s, PieceType pt); -Bitboard line_bb(Square s1, Square s2); -Bitboard between_bb(Square s1, Square s2); -Bitboard ray_pass_bb(Square s1, Square s2); + +#endif + +Bitboard line_bb(Square s1, Square s2); +Bitboard between_bb(Square s1, Square s2); +Bitboard ray_pass_bb(Square s1, Square s2); // Returns the bitboard of target square for the given step // from the given square. If the step is off the board, returns empty bitboard. @@ -220,6 +280,21 @@ inline Bitboard attacks_bb(Square s, Bitboard occupied) { assert(Pt != PAWN && is_ok(s)); +#ifdef USE_DUAL_HYPERBOLA_QUINT + const auto [bishop, rook] = dual_magic(s).both_attacks_bb(occupied); + + switch (Pt) + { + case BISHOP : + return bishop; + case ROOK : + return rook; + case QUEEN : + return bishop | rook; + default : + return PseudoAttacks[Pt][s]; + } +#else switch (Pt) { case BISHOP : @@ -230,6 +305,7 @@ inline Bitboard attacks_bb(Square s, Bitboard occupied) { default : return PseudoAttacks[Pt][s]; } +#endif } // Returns the attacks by the given piece diff --git a/src/misc.h b/src/misc.h index d763c50d3..9059762ff 100644 --- a/src/misc.h +++ b/src/misc.h @@ -544,6 +544,14 @@ void move_to_front(std::vector& vec, Predicate pred) { #define sf_unreachable() #endif +#ifdef __GNUC__ + #define RESTRICT __restrict__ +#elif defined(_MSC_VER) + #define RESTRICT __restrict +#else + #define RESTRICT +#endif + } // namespace Stockfish template