diff --git a/src/tt.cpp b/src/tt.cpp index bd22e497c..b99a769f4 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -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 TranspositionTable::probe(const Key key) const { TTEntry* const tte = first_entry(key); @@ -234,12 +227,11 @@ std::tuple 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)}; } diff --git a/src/tt.h b/src/tt.h index 38f6c8f4f..584b92bff 100644 --- a/src/tt.h +++ b/src/tt.h @@ -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 - 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 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 diff --git a/src/types.h b/src/types.h index 23543e478..c109dbb49 100644 --- a/src/types.h +++ b/src/types.h @@ -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 {