Hyperbola quintessence for ARM

Passed STC

https://tests.stockfishchess.org/tests/view/69f44c1e1e5788938e86aa2a
LLR: 2.93 (-2.94,2.94) <0.00,2.00>
Total: 27680 W: 7340 L: 7053 D: 13287
Ptnml(0-2): 72, 2862, 7683, 3153, 70

x86 is unaffected

Data from Joost's machine (Neoverse V2):

```
==== master ====
==== Bench: 2723949 ====
1 Nodes/second : 276819933
2 Nodes/second : 273971948
3 Nodes/second : 275567995
Average (over 3):  275453292
==== 5b54755db23a3de29407b805b85e7bbdef28df49 ====
==== Bench: 2723949 ====
1 Nodes/second : 282861542
2 Nodes/second : 279085277
3 Nodes/second : 279612223
Average (over 3):  280519680 (+1.8%)
```

Data from my M1 macbook

```
Result of 100 runs
==================
base (./stockfish    ) =    1474702  +/- 6169
test (...fish.rbit-pr) =    1500236  +/- 6690
diff                   =     +25533  +/- 4011

speedup        = +0.0173
P(speedup > 0) =  1.0000
```

Hyperbola quintessence works nicely on ARM because it has an efficient bit reversal instruction `rbit`. This lets us avoid the large magic bitboards lookup table. Codegen looks nice (this is for rook attacks I think):

```
0000000100001ecc <__ZN9Stockfish10attacks_bbILNS_9PieceTypeE3EEEyNS_6SquareEy>:
100001ecc: 2a0003e8     mov w8, w0
100001ed0: f002cb09     adrp x9, 0x105964000 <dyld_stub_binder+0x105964000>
100001ed4: 91200129     add x9, x9, #0x800
100001ed8: 8b081928     add x8, x9, x8, lsl #6
100001edc: a9402909     ldp x9, x10, [x8]
100001ee0: 8a01012b     and x11, x9, x1
100001ee4: dac0016c     rbit x12, x11
100001ee8: a941210d     ldp x13, x8, [x8, #0x10]
100001eec: cb08018c     sub x12, x12, x8
100001ef0: dac0018c     rbit x12, x12
100001ef4: cb0d016b     sub x11, x11, x13
100001ef8: ca0b018b     eor x11, x12, x11
100001efc: 8a090169     and x9, x11, x9
100001f00: 8a01014b     and x11, x10, x1
100001f04: dac0016c     rbit x12, x11
100001f08: cb080188     sub x8, x12, x8
100001f0c: dac00108     rbit x8, x8
100001f10: cb0d016b     sub x11, x11, x13
100001f14: ca0b0108     eor x8, x8, x11
100001f18: 8a0a0108     and x8, x8, x10
100001f1c: 8b090100     add x0, x8, x9
100001f20: d65f03c0     ret
```

closes https://github.com/official-stockfish/Stockfish/pull/6795

No functional change
This commit is contained in:
anematode
2026-05-04 08:32:23 +02:00
committed by Joost VandeVondele
parent fe1d2b456c
commit dc1686345c
2 changed files with 96 additions and 30 deletions
+59 -21
View File
@@ -62,6 +62,39 @@ std::string Bitboards::pretty(Bitboard b) {
return s; return s;
} }
#ifdef USE_HYPERBOLA_QUINT
static Bitboard line_mask(Square sq, Direction d1, Direction d2) {
Bitboard mask = 0, dest;
for (Direction d : {d1, d2})
{
Square s = sq;
while ((dest = Bitboards::safe_destination(s, d)))
{
mask |= dest;
s += d;
}
}
return mask;
}
static void init_magics(Magic magics[][2]) {
for (Square s = SQ_A1; s <= SQ_H8; ++s)
{
Magic& rook = magics[s][ROOK - BISHOP];
rook.mask1 = line_mask(s, NORTH, SOUTH);
rook.mask2 = line_mask(s, EAST, WEST);
Magic& bishop = magics[s][BISHOP - BISHOP];
bishop.mask1 = line_mask(s, NORTH_EAST, SOUTH_WEST);
bishop.mask2 = line_mask(s, NORTH_WEST, SOUTH_EAST);
rook.r = bishop.r = square_bb(s) * 2;
rook.rr = bishop.rr = square_bb(Square(63 - int(s))) * 2;
}
}
#else
namespace { namespace {
[[maybe_unused]] constexpr Bitboard constexpr_pext(Bitboard b, Bitboard m) { [[maybe_unused]] constexpr Bitboard constexpr_pext(Bitboard b, Bitboard m) {
Bitboard result = 0, bit = 0; Bitboard result = 0, bit = 0;
@@ -74,23 +107,23 @@ namespace {
return result; return result;
} }
// Computes all rook and bishop attacks at startup or optionally, compile time. Magic // Computes all rook and bishop attacks at startup or optionally, compile time. Magic
// bitboards are used to look up attacks of sliding pieces. As a reference see // bitboards are used to look up attacks of sliding pieces. As a reference see
// https://www.chessprogramming.org/Magic_Bitboards. In particular, here we use // https://www.chessprogramming.org/Magic_Bitboards. In particular, here we use
// the so called "fancy" approach. // the so called "fancy" approach.
#ifdef USE_COMPTIME_ATTACKS #ifdef USE_COMPTIME_ATTACKS
constexpr constexpr
#endif #endif
void void
init_magics(PieceType pt, init_magics(PieceType pt,
MagicMask table[], MagicMask table[],
Magic magics[][2], Magic magics[][2],
[[maybe_unused]] bool tableAlreadyInit) { [[maybe_unused]] bool tableAlreadyInit) {
#if !defined(USE_COMPTIME_ATTACKS) #if !defined(USE_COMPTIME_ATTACKS)
tableAlreadyInit = false; tableAlreadyInit = false;
#endif #endif
#ifndef USE_PEXT #ifndef USE_PEXT
// Optimal PRNG seeds to pick the correct magics in the shortest time // Optimal PRNG seeds to pick the correct magics in the shortest time
int seeds[][RANK_NB] = {{8977, 44560, 54343, 38998, 5731, 95205, 104912, 17020}, int seeds[][RANK_NB] = {{8977, 44560, 54343, 38998, 5731, 95205, 104912, 17020},
{728, 10316, 55013, 32803, 12281, 15100, 16645, 255}}; {728, 10316, 55013, 32803, 12281, 15100, 16645, 255}};
@@ -98,7 +131,7 @@ constexpr
Bitboard occupancy[4096]; Bitboard occupancy[4096];
int epoch[4096] = {}, cnt = 0; int epoch[4096] = {}, cnt = 0;
Bitboard reference[4096] = {}; Bitboard reference[4096] = {};
#endif #endif
int size = 0; int size = 0;
for (Square s = SQ_A1; s <= SQ_H8; ++s) for (Square s = SQ_A1; s <= SQ_H8; ++s)
@@ -114,11 +147,11 @@ constexpr
Magic& m = magics[s][pt - BISHOP]; Magic& m = magics[s][pt - BISHOP];
Bitboard attacks = Bitboards::sliding_attack(pt, s, 0); Bitboard attacks = Bitboards::sliding_attack(pt, s, 0);
m.mask = attacks & ~edges; m.mask = attacks & ~edges;
#ifdef USE_PEXT #ifdef USE_PEXT
m.pseudoAttacks = attacks; m.pseudoAttacks = attacks;
#else #else
m.shift = (Is64Bit ? 64 : 32) - popcount(m.mask); m.shift = (Is64Bit ? 64 : 32) - popcount(m.mask);
#endif #endif
// Set the offset for the attacks table of the square. We have individual // Set the offset for the attacks table of the square. We have individual
// table sizes for each square with "Fancy Magic Bitboards". // table sizes for each square with "Fancy Magic Bitboards".
m.attacks = s == SQ_A1 ? table : magics[s - 1][pt - BISHOP].attacks + size; m.attacks = s == SQ_A1 ? table : magics[s - 1][pt - BISHOP].attacks + size;
@@ -130,7 +163,7 @@ constexpr
[[maybe_unused]] Bitboard prevSliding = -1; [[maybe_unused]] Bitboard prevSliding = -1;
do do
{ {
#ifdef USE_PEXT #ifdef USE_PEXT
if (!tableAlreadyInit) if (!tableAlreadyInit)
{ {
Bitboard sliding = Bitboards::sliding_attack(pt, s, b); Bitboard sliding = Bitboards::sliding_attack(pt, s, b);
@@ -138,16 +171,16 @@ constexpr
sliding != prevSliding ? constexpr_pext(sliding, attacks) : m.attacks[size - 1]; sliding != prevSliding ? constexpr_pext(sliding, attacks) : m.attacks[size - 1];
prevSliding = sliding; prevSliding = sliding;
} }
#else #else
occupancy[size] = b; occupancy[size] = b;
reference[size] = Bitboards::sliding_attack(pt, s, b); reference[size] = Bitboards::sliding_attack(pt, s, b);
#endif #endif
size++; size++;
b = (b - m.mask) & m.mask; b = (b - m.mask) & m.mask;
} while (b); } while (b);
#ifndef USE_PEXT #ifndef USE_PEXT
PRNG rng(seeds[Is64Bit][rank_of(s)]); PRNG rng(seeds[Is64Bit][rank_of(s)]);
// Find a magic for square 's' picking up an (almost) random number // Find a magic for square 's' picking up an (almost) random number
@@ -176,11 +209,11 @@ constexpr
break; break;
} }
} }
#endif #endif
} }
} }
#if defined(USE_COMPTIME_ATTACKS) && defined(USE_PEXT) #if defined(USE_COMPTIME_ATTACKS) && defined(USE_PEXT)
constexpr auto RookTable = []() { constexpr auto RookTable = []() {
std::array<uint16_t, 0x19000> result{}; std::array<uint16_t, 0x19000> result{};
Magic magics[64][2] = {}; Magic magics[64][2] = {};
@@ -193,12 +226,13 @@ constexpr auto BishopTable = []() {
init_magics(BISHOP, result.data(), magics, false); init_magics(BISHOP, result.data(), magics, false);
return result; return result;
}(); }();
#else #else
std::array<MagicMask, 0x19000> RookTable; std::array<MagicMask, 0x19000> RookTable;
std::array<MagicMask, 0x1480> BishopTable; std::array<MagicMask, 0x1480> BishopTable;
#endif #endif
} }
#endif
// Initializes various bitboard tables. It is called at // Initializes various bitboard tables. It is called at
// startup and relies on global objects to be already zero-initialized. // startup and relies on global objects to be already zero-initialized.
@@ -211,8 +245,12 @@ void Bitboards::init() {
for (Square s2 = SQ_A1; s2 <= SQ_H8; ++s2) for (Square s2 = SQ_A1; s2 <= SQ_H8; ++s2)
SquareDistance[s1][s2] = std::max(distance<File>(s1, s2), distance<Rank>(s1, s2)); SquareDistance[s1][s2] = std::max(distance<File>(s1, s2), distance<Rank>(s1, s2));
#ifdef USE_HYPERBOLA_QUINT
init_magics(Magics);
#else
init_magics(ROOK, const_cast<MagicMask*>(RookTable.data()), Magics, true); init_magics(ROOK, const_cast<MagicMask*>(RookTable.data()), Magics, true);
init_magics(BISHOP, const_cast<MagicMask*>(BishopTable.data()), Magics, true); init_magics(BISHOP, const_cast<MagicMask*>(BishopTable.data()), Magics, true);
#endif
for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1) for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1)
{ {
+37 -9
View File
@@ -31,6 +31,11 @@
#include "types.h" #include "types.h"
#ifdef __aarch64__
#include <arm_acle.h>
#define USE_HYPERBOLA_QUINT
#endif
namespace Stockfish { namespace Stockfish {
namespace Bitboards { namespace Bitboards {
@@ -74,41 +79,64 @@ extern Bitboard BetweenBB[SQUARE_NB][SQUARE_NB];
extern Bitboard LineBB[SQUARE_NB][SQUARE_NB]; extern Bitboard LineBB[SQUARE_NB][SQUARE_NB];
extern Bitboard RayPassBB[SQUARE_NB][SQUARE_NB]; extern Bitboard RayPassBB[SQUARE_NB][SQUARE_NB];
#ifdef USE_HYPERBOLA_QUINT
// Hyperbola quintessence implementation for ARM, thanks to the availability of an
// efficient bit reversal instruction.
// See https://www.chessprogramming.org/Hyperbola_Quintessence
struct Magic {
// For rooks: file attacks, rank attacks. For bishops: diagonal/antidiagonal
Bitboard mask1, mask2;
// Precomputed 2 * square_bb(sq), 2 * reverse(square_bb(sq))
Bitboard r, rr;
Bitboard hyperbola(Bitboard occupied, Bitboard mask) const {
Bitboard o = occupied & mask;
Bitboard fwd = o - r;
Bitboard rev = __rbitll(o) - rr;
return (fwd ^ __rbitll(rev)) & mask;
}
Bitboard attacks_bb(Bitboard occupied) const {
return hyperbola(occupied, mask1) | hyperbola(occupied, mask2);
}
};
#else
// Magic holds all magic bitboards relevant data for a single square // Magic holds all magic bitboards relevant data for a single square
struct Magic { struct Magic {
Bitboard mask; Bitboard mask;
#ifdef USE_PEXT #ifdef USE_PEXT
uint16_t* attacks; uint16_t* attacks;
Bitboard pseudoAttacks; Bitboard pseudoAttacks;
#else #else
Bitboard* attacks; Bitboard* attacks;
Bitboard magic; Bitboard magic;
unsigned shift; unsigned shift;
#endif #endif
// Compute the attack's index using the 'magic bitboards' approach // Compute the attack's index using the 'magic bitboards' approach
unsigned index(Bitboard occupied) const { unsigned index(Bitboard occupied) const {
#ifdef USE_PEXT #ifdef USE_PEXT
return unsigned(pext(occupied, mask)); return unsigned(pext(occupied, mask));
#else #else
if (Is64Bit) if (Is64Bit)
return unsigned(((occupied & mask) * magic) >> shift); return unsigned(((occupied & mask) * magic) >> shift);
unsigned lo = unsigned(occupied) & unsigned(mask); unsigned lo = unsigned(occupied) & unsigned(mask);
unsigned hi = unsigned(occupied >> 32) & unsigned(mask >> 32); unsigned hi = unsigned(occupied >> 32) & unsigned(mask >> 32);
return (lo * unsigned(magic) ^ hi * unsigned(magic >> 32)) >> shift; return (lo * unsigned(magic) ^ hi * unsigned(magic >> 32)) >> shift;
#endif #endif
} }
Bitboard attacks_bb(Bitboard occupied) const { Bitboard attacks_bb(Bitboard occupied) const {
#ifdef USE_PEXT #ifdef USE_PEXT
return pdep(attacks[index(occupied)], pseudoAttacks); return pdep(attacks[index(occupied)], pseudoAttacks);
#else #else
return attacks[index(occupied)]; return attacks[index(occupied)];
#endif #endif
} }
}; };
#endif
extern Magic Magics[SQUARE_NB][2]; extern Magic Magics[SQUARE_NB][2];