mirror of
https://github.com/official-stockfish/Stockfish.git
synced 2026-07-22 20:57:10 +00:00
Simplify relative_age and its constants
STC: https://tests.stockfishchess.org/tests/view/69cf29ed1668971c9da23a5a LLR: 3.47 (-2.94,2.94) <-1.75,0.25> Total: 193760 W: 50142 L: 50051 D: 93567 Ptnml(0-2): 534, 21375, 52980, 21448, 543 * Simplify retval and arithmetic semantics * Remove mixing of int and uint8_t I find this overall much easier to reason about (Also rename a DEPTH constant per mstembera's suggestion https://github.com/official-stockfish/Stockfish/pull/5766#issuecomment-2586449296) closes https://github.com/official-stockfish/Stockfish/pull/6702 No functional change
This commit is contained in:
committed by
Joost VandeVondele
parent
9bdf2f8d4c
commit
3cce652564
+50
-58
@@ -32,33 +32,46 @@
|
||||
namespace Stockfish {
|
||||
|
||||
|
||||
// TTEntry struct is the 10 bytes transposition table entry, defined as below:
|
||||
// TTEntry struct is the 10 bytes transposition table entry, defined as:
|
||||
//
|
||||
// key 16 bit
|
||||
// depth 8 bit
|
||||
// generation 5 bit
|
||||
// pv node 1 bit
|
||||
// bound type 2 bit
|
||||
// generation 5 bit
|
||||
// move 16 bit
|
||||
// value 16 bit
|
||||
// evaluation 16 bit
|
||||
//
|
||||
// These fields are in the same order as accessed by TT::probe(), since memory is fastest sequentially.
|
||||
// Equally, the store order in save() matches this order.
|
||||
//
|
||||
// We use `bool(depth8)` as the cheap internal occupancy check, corresponding to `depth == DEPTH_NONE`
|
||||
// externally, so we offset the internal depth by DEPTH_NONE.
|
||||
//
|
||||
// Pv, bound and generation are packed in a single byte.
|
||||
static constexpr uint8_t GENERATION_BITS = 5;
|
||||
static constexpr uint8_t GENERATION_MASK = (1 << GENERATION_BITS) - 1;
|
||||
static constexpr uint8_t BOUND_SHIFT = GENERATION_BITS;
|
||||
static constexpr uint8_t BOUND_MASK = 0b11 << BOUND_SHIFT;
|
||||
static constexpr uint8_t PV_SHIFT = BOUND_SHIFT + 2;
|
||||
static constexpr uint8_t PV_MASK = 1 << PV_SHIFT;
|
||||
|
||||
struct TTEntry {
|
||||
|
||||
// Convert internal bitfields to external types
|
||||
TTData read() const {
|
||||
return TTData{Move(move16), Value(value16),
|
||||
Value(eval16), Depth(depth8 + DEPTH_ENTRY_OFFSET),
|
||||
Bound(genBound8 & 0x3), bool(genBound8 & 0x4)};
|
||||
return TTData{Move(move16),
|
||||
Value(value16),
|
||||
Value(eval16),
|
||||
Depth(DEPTH_NONE + depth8),
|
||||
Bound((genBound8 & BOUND_MASK) >> BOUND_SHIFT),
|
||||
bool(genBound8 & PV_MASK)};
|
||||
}
|
||||
|
||||
bool is_occupied() const;
|
||||
void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8);
|
||||
// The returned age is a multiple of GENERATION_DELTA
|
||||
uint8_t relative_age(const uint8_t generation8) const;
|
||||
bool is_occupied() const { return bool(depth8); };
|
||||
void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t curr_generation);
|
||||
uint8_t relative_age(const uint8_t curr_generation) const;
|
||||
|
||||
private:
|
||||
friend class TranspositionTable;
|
||||
@@ -71,55 +84,37 @@ struct TTEntry {
|
||||
int16_t eval16;
|
||||
};
|
||||
|
||||
// `genBound8` is where most of the details are. We use the following constants to manipulate 5 leading generation bits
|
||||
// and 3 trailing miscellaneous bits.
|
||||
|
||||
// These bits are reserved for other things.
|
||||
static constexpr unsigned GENERATION_BITS = 3;
|
||||
// increment for generation field
|
||||
static constexpr int GENERATION_DELTA = (1 << GENERATION_BITS);
|
||||
// cycle length
|
||||
static constexpr int GENERATION_CYCLE = 255 + GENERATION_DELTA;
|
||||
// mask to pull out generation number
|
||||
static constexpr int GENERATION_MASK = (0xFF << GENERATION_BITS) & 0xFF;
|
||||
|
||||
// DEPTH_ENTRY_OFFSET exists because 1) we use `bool(depth8)` as the occupancy check, but
|
||||
// 2) we need to store negative depths for QS. (`depth8` is the only field with "spare bits":
|
||||
// we sacrifice the ability to store depths greater than 1<<8 less the offset, as asserted in `save`.)
|
||||
bool TTEntry::is_occupied() const { return bool(depth8); }
|
||||
|
||||
// Populates the TTEntry with a new node's data, possibly
|
||||
// overwriting an old position. The update is not atomic and can be racy.
|
||||
// overwriting an old position. The update is non-atomic and can be racy.
|
||||
void TTEntry::save(
|
||||
Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8) {
|
||||
Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t curr_generation) {
|
||||
|
||||
// Preserve the old ttmove if we don't have a new one
|
||||
if (m || uint16_t(k) != key16)
|
||||
move16 = m;
|
||||
|
||||
// Overwrite less valuable entries (cheapest checks first)
|
||||
if (b == BOUND_EXACT || uint16_t(k) != key16 || d - DEPTH_ENTRY_OFFSET + 2 * pv > depth8 - 4
|
||||
|| relative_age(generation8))
|
||||
if (b == BOUND_EXACT || uint16_t(k) != key16 || d - DEPTH_NONE + 2 * pv > depth8 - 4
|
||||
|| relative_age(curr_generation))
|
||||
{
|
||||
assert(d > DEPTH_ENTRY_OFFSET);
|
||||
assert(d < 256 + DEPTH_ENTRY_OFFSET);
|
||||
assert(d > DEPTH_NONE);
|
||||
assert(d - DEPTH_NONE < 256);
|
||||
assert(curr_generation <= GENERATION_MASK); // TT::new_search() plays nice
|
||||
|
||||
key16 = uint16_t(k);
|
||||
depth8 = uint8_t(d - DEPTH_ENTRY_OFFSET);
|
||||
genBound8 = uint8_t(generation8 | uint8_t(pv) << 2 | b);
|
||||
depth8 = uint8_t(d - DEPTH_NONE);
|
||||
genBound8 = uint8_t(curr_generation | b << BOUND_SHIFT | uint8_t(pv) << PV_SHIFT);
|
||||
value16 = int16_t(v);
|
||||
eval16 = int16_t(ev);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uint8_t TTEntry::relative_age(const uint8_t generation8) const {
|
||||
// Due to our packed storage format for generation and its cyclic
|
||||
// nature we add GENERATION_CYCLE (256 is the modulus, plus what
|
||||
// is needed to keep the unrelated lowest n bits from affecting
|
||||
// the result) to calculate the entry age correctly even after
|
||||
// generation8 overflows into the next cycle.
|
||||
return (GENERATION_CYCLE + generation8 - genBound8) & GENERATION_MASK;
|
||||
uint8_t TTEntry::relative_age(const uint8_t curr_generation) const {
|
||||
// Returns this entry's age. We count generations like clocks count hours,
|
||||
// i.e. we require 0 - 1 == 31. Unsigned subtraction guarantees the required
|
||||
// borrowing regardless of the upper pv/bound bits.
|
||||
return (curr_generation - genBound8) & GENERATION_MASK;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,8 +123,8 @@ TTWriter::TTWriter(TTEntry* tte) :
|
||||
entry(tte) {}
|
||||
|
||||
void TTWriter::write(
|
||||
Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8) {
|
||||
entry->save(k, v, pv, b, d, m, ev, generation8);
|
||||
Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t curr_generation) {
|
||||
entry->save(k, v, pv, b, d, m, ev, curr_generation);
|
||||
}
|
||||
|
||||
|
||||
@@ -192,34 +187,32 @@ void TranspositionTable::clear(ThreadPool& threads) {
|
||||
|
||||
// Returns an approximation of the hashtable
|
||||
// occupation during a search. The hash is x permill full, as per UCI protocol.
|
||||
// Only counts entries which match the current generation.
|
||||
// Only counts entries which are younger than maxAge.
|
||||
int TranspositionTable::hashfull(int maxAge) const {
|
||||
int maxAgeInternal = maxAge << GENERATION_BITS;
|
||||
int cnt = 0;
|
||||
int cnt = 0;
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
for (int j = 0; j < ClusterSize; ++j)
|
||||
cnt += table[i].entry[j].is_occupied()
|
||||
&& table[i].entry[j].relative_age(generation8) <= maxAgeInternal;
|
||||
&& table[i].entry[j].relative_age(generation8) <= maxAge;
|
||||
|
||||
return cnt / ClusterSize;
|
||||
}
|
||||
|
||||
|
||||
void TranspositionTable::new_search() {
|
||||
// increment by delta to keep lower bits as is
|
||||
generation8 += GENERATION_DELTA;
|
||||
++generation8;
|
||||
// Don't overflow into the other bits of TTEntry::genBound8
|
||||
generation8 &= GENERATION_MASK;
|
||||
}
|
||||
|
||||
|
||||
uint8_t TranspositionTable::generation() const { return generation8; }
|
||||
|
||||
|
||||
// Looks up the current position in the transposition
|
||||
// table. It returns true if the position is found.
|
||||
// Looks up the current position in the transposition table.
|
||||
// It returns true if the key is found (which may be a collision), and has non-null data.
|
||||
// Otherwise, it returns false and a pointer to an empty or least valuable TTEntry
|
||||
// to be replaced later. The replace value of an entry is calculated as its depth
|
||||
// minus 8 times its relative age. TTEntry t1 is considered more valuable than
|
||||
// TTEntry t2 if its replace value is greater than that of t2.
|
||||
// to be replaced later. The value of an entry is its depth minus 8 times its relative age.
|
||||
std::tuple<bool, TTData, TTWriter> TranspositionTable::probe(const Key key) const {
|
||||
|
||||
TTEntry* const tte = first_entry(key);
|
||||
@@ -234,12 +227,11 @@ std::tuple<bool, TTData, TTWriter> TranspositionTable::probe(const Key key) cons
|
||||
// Find an entry to be replaced according to the replacement strategy
|
||||
TTEntry* replace = tte;
|
||||
for (int i = 1; i < ClusterSize; ++i)
|
||||
if (replace->depth8 - replace->relative_age(generation8)
|
||||
> tte[i].depth8 - tte[i].relative_age(generation8))
|
||||
if (replace->depth8 - 8 * replace->relative_age(generation8)
|
||||
> tte[i].depth8 - 8 * tte[i].relative_age(generation8))
|
||||
replace = &tte[i];
|
||||
|
||||
return {false,
|
||||
TTData{Move::none(), VALUE_NONE, VALUE_NONE, DEPTH_ENTRY_OFFSET, BOUND_NONE, false},
|
||||
return {false, TTData{Move::none(), VALUE_NONE, VALUE_NONE, DEPTH_NONE, BOUND_NONE, false},
|
||||
TTWriter(replace)};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,14 +37,11 @@ struct Cluster;
|
||||
// thus elo. As a hash table, collisions are possible and may cause chess playing issues (bizarre blunders, faulty mate
|
||||
// reports, etc). Fixing these also loses elo; however such risk decreases quickly with larger TT size.
|
||||
//
|
||||
// `probe` is the primary method: given a board position, we lookup its entry in the table, and return a tuple of:
|
||||
// 1) whether the entry already has this position
|
||||
// 2) a copy of the prior data (if any) (may be inconsistent due to read races)
|
||||
// 3) a writer object to this entry
|
||||
// The copied data and the writer are separated to maintain clear boundaries between local vs global objects.
|
||||
// We clearly separate TTData, a local copy of an entry, from TTWriter, which writes to the global table.
|
||||
|
||||
|
||||
// A copy of the data already in the entry (possibly collided). `probe` may be racy, resulting in inconsistent data.
|
||||
// A copy of the data already in an entry (possibly collided). Probes and reads are racy and non-atomic,
|
||||
// possibly resulting in inconsistent data.
|
||||
struct TTData {
|
||||
Move move;
|
||||
Value value, eval;
|
||||
@@ -66,7 +63,8 @@ struct TTData {
|
||||
};
|
||||
|
||||
|
||||
// This is used to make racy writes to the global TT.
|
||||
// This is used to make racy, non-atomic writes to the global TT. Writes are not "guaranteed":
|
||||
// for chess reasons, we may decide the new data is less important than the old.
|
||||
struct TTWriter {
|
||||
public:
|
||||
void write(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8);
|
||||
@@ -83,18 +81,22 @@ class TranspositionTable {
|
||||
public:
|
||||
~TranspositionTable() { aligned_large_pages_free(table); }
|
||||
|
||||
void resize(size_t mbSize, ThreadPool& threads); // Set TT size
|
||||
void resize(size_t mbSize, ThreadPool& threads); // Set TT size in MiB
|
||||
void clear(ThreadPool& threads); // Re-initialize memory, multithreaded
|
||||
int hashfull(int maxAge = 0)
|
||||
const; // Approximate what fraction of entries (permille) have been written to during this root search
|
||||
|
||||
void
|
||||
new_search(); // This must be called at the beginning of each root search to track entry aging
|
||||
uint8_t generation() const; // The current age, used when writing new data to the TT
|
||||
std::tuple<bool, TTData, TTWriter>
|
||||
probe(const Key key) const; // The main method, whose retvals separate local vs global objects
|
||||
TTEntry* first_entry(const Key key)
|
||||
const; // This is the hash function; its only external use is memory prefetching.
|
||||
// Approximate what fraction of entries (permille) have been written to during this root search
|
||||
int hashfull(int maxAge = 0) const;
|
||||
|
||||
// `probe` is the primary method: given a board position, we lookup its entry in the table, and return a tuple of:
|
||||
// 1) whether the entry already had data on this position
|
||||
// 2) a copy of the prior data, if any (may be self-inconsistent due to read races)
|
||||
// 3) a writer object to the entry
|
||||
std::tuple<bool, TTData, TTWriter> probe(const Key key) const;
|
||||
// The hash function; its only external use is memory prefetching
|
||||
TTEntry* first_entry(const Key key) const;
|
||||
|
||||
private:
|
||||
friend struct TTEntry;
|
||||
@@ -102,7 +104,7 @@ class TranspositionTable {
|
||||
size_t clusterCount;
|
||||
Cluster* table = nullptr;
|
||||
|
||||
uint8_t generation8 = 0; // Size must be not bigger than TTEntry::genBound8
|
||||
uint8_t generation8 = 0;
|
||||
};
|
||||
|
||||
} // namespace Stockfish
|
||||
|
||||
+5
-5
@@ -220,11 +220,11 @@ using Depth = int;
|
||||
constexpr Depth DEPTH_QS = 0;
|
||||
// For transposition table entries where no searching at all was done
|
||||
// (whether regular or qsearch) we use DEPTH_UNSEARCHED, which should thus
|
||||
// compare lower than any quiescence or regular depth. DEPTH_ENTRY_OFFSET
|
||||
// is used only for the transposition table entry occupancy check (see tt.cpp),
|
||||
// and should thus be lower than DEPTH_UNSEARCHED.
|
||||
constexpr Depth DEPTH_UNSEARCHED = -2;
|
||||
constexpr Depth DEPTH_ENTRY_OFFSET = -3;
|
||||
// compare lower than any quiescence or regular depth. DEPTH_NONE is used
|
||||
// for the transposition table entry occupancy check (see tt.cpp), and
|
||||
// should thus be lower than DEPTH_UNSEARCHED.
|
||||
constexpr Depth DEPTH_UNSEARCHED = -2;
|
||||
constexpr Depth DEPTH_NONE = -3;
|
||||
|
||||
// clang-format off
|
||||
enum Square : uint8_t {
|
||||
|
||||
Reference in New Issue
Block a user