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
This commit is contained in:
anematode
2026-05-25 11:20:24 +02:00
committed by Joost VandeVondele
parent 41f3557db9
commit 77a8f6ccf3
3 changed files with 143 additions and 7 deletions
+80 -4
View File
@@ -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 <immintrin.h>
#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<Bitboard, Bitboard> 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<const __m256i*>(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