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;
}
#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 {
[[maybe_unused]] constexpr Bitboard constexpr_pext(Bitboard b, Bitboard m) {
Bitboard result = 0, bit = 0;
@@ -74,23 +107,23 @@ namespace {
return result;
}
// 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
// https://www.chessprogramming.org/Magic_Bitboards. In particular, here we use
// the so called "fancy" approach.
#ifdef USE_COMPTIME_ATTACKS
// 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
// https://www.chessprogramming.org/Magic_Bitboards. In particular, here we use
// the so called "fancy" approach.
#ifdef USE_COMPTIME_ATTACKS
constexpr
#endif
#endif
void
init_magics(PieceType pt,
MagicMask table[],
Magic magics[][2],
[[maybe_unused]] bool tableAlreadyInit) {
#if !defined(USE_COMPTIME_ATTACKS)
#if !defined(USE_COMPTIME_ATTACKS)
tableAlreadyInit = false;
#endif
#endif
#ifndef USE_PEXT
#ifndef USE_PEXT
// Optimal PRNG seeds to pick the correct magics in the shortest time
int seeds[][RANK_NB] = {{8977, 44560, 54343, 38998, 5731, 95205, 104912, 17020},
{728, 10316, 55013, 32803, 12281, 15100, 16645, 255}};
@@ -98,7 +131,7 @@ constexpr
Bitboard occupancy[4096];
int epoch[4096] = {}, cnt = 0;
Bitboard reference[4096] = {};
#endif
#endif
int size = 0;
for (Square s = SQ_A1; s <= SQ_H8; ++s)
@@ -114,11 +147,11 @@ constexpr
Magic& m = magics[s][pt - BISHOP];
Bitboard attacks = Bitboards::sliding_attack(pt, s, 0);
m.mask = attacks & ~edges;
#ifdef USE_PEXT
#ifdef USE_PEXT
m.pseudoAttacks = attacks;
#else
#else
m.shift = (Is64Bit ? 64 : 32) - popcount(m.mask);
#endif
#endif
// Set the offset for the attacks table of the square. We have individual
// table sizes for each square with "Fancy Magic Bitboards".
m.attacks = s == SQ_A1 ? table : magics[s - 1][pt - BISHOP].attacks + size;
@@ -130,7 +163,7 @@ constexpr
[[maybe_unused]] Bitboard prevSliding = -1;
do
{
#ifdef USE_PEXT
#ifdef USE_PEXT
if (!tableAlreadyInit)
{
Bitboard sliding = Bitboards::sliding_attack(pt, s, b);
@@ -138,16 +171,16 @@ constexpr
sliding != prevSliding ? constexpr_pext(sliding, attacks) : m.attacks[size - 1];
prevSliding = sliding;
}
#else
#else
occupancy[size] = b;
reference[size] = Bitboards::sliding_attack(pt, s, b);
#endif
#endif
size++;
b = (b - m.mask) & m.mask;
} while (b);
#ifndef USE_PEXT
#ifndef USE_PEXT
PRNG rng(seeds[Is64Bit][rank_of(s)]);
// Find a magic for square 's' picking up an (almost) random number
@@ -176,11 +209,11 @@ constexpr
break;
}
}
#endif
#endif
}
}
#if defined(USE_COMPTIME_ATTACKS) && defined(USE_PEXT)
#if defined(USE_COMPTIME_ATTACKS) && defined(USE_PEXT)
constexpr auto RookTable = []() {
std::array<uint16_t, 0x19000> result{};
Magic magics[64][2] = {};
@@ -193,12 +226,13 @@ constexpr auto BishopTable = []() {
init_magics(BISHOP, result.data(), magics, false);
return result;
}();
#else
#else
std::array<MagicMask, 0x19000> RookTable;
std::array<MagicMask, 0x1480> BishopTable;
#endif
#endif
}
#endif
// Initializes various bitboard tables. It is called at
// 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)
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(BISHOP, const_cast<MagicMask*>(BishopTable.data()), Magics, true);
#endif
for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1)
{
+37 -9
View File
@@ -31,6 +31,11 @@
#include "types.h"
#ifdef __aarch64__
#include <arm_acle.h>
#define USE_HYPERBOLA_QUINT
#endif
namespace Stockfish {
namespace Bitboards {
@@ -74,41 +79,64 @@ extern Bitboard BetweenBB[SQUARE_NB][SQUARE_NB];
extern Bitboard LineBB[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
struct Magic {
Bitboard mask;
#ifdef USE_PEXT
#ifdef USE_PEXT
uint16_t* attacks;
Bitboard pseudoAttacks;
#else
#else
Bitboard* attacks;
Bitboard magic;
unsigned shift;
#endif
#endif
// Compute the attack's index using the 'magic bitboards' approach
unsigned index(Bitboard occupied) const {
#ifdef USE_PEXT
#ifdef USE_PEXT
return unsigned(pext(occupied, mask));
#else
#else
if (Is64Bit)
return unsigned(((occupied & mask) * magic) >> shift);
unsigned lo = unsigned(occupied) & unsigned(mask);
unsigned hi = unsigned(occupied >> 32) & unsigned(mask >> 32);
return (lo * unsigned(magic) ^ hi * unsigned(magic >> 32)) >> shift;
#endif
#endif
}
Bitboard attacks_bb(Bitboard occupied) const {
#ifdef USE_PEXT
#ifdef USE_PEXT
return pdep(attacks[index(occupied)], pseudoAttacks);
#else
#else
return attacks[index(occupied)];
#endif
#endif
}
};
#endif
extern Magic Magics[SQUARE_NB][2];