summaryrefslogtreecommitdiff
path: root/src/fathom
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-25 08:44:25 -0700
committerScott Gasch <[email protected]>2026-09-25 08:44:25 -0700
commit9b28080f8b3dca4588cb2aaaf748451f45e3c626 (patch)
tree54e0aff5d6ee1520c590c7c04a5ae2f8299e7497 /src/fathom
parent66740dc89c7351c120c25788d0079055034f4942 (diff)
Two ideas:
1. Nullmove R=3 + depth/6 along with some ShouldWeDoNullmoveSearch tweaking and calibrating. 2. Probe EGTB everywhere in the search as long as the table/position is resident in memory.
Diffstat (limited to 'src/fathom')
-rw-r--r--src/fathom/tbprobe.c236
-rw-r--r--src/fathom/tbprobe.h58
2 files changed, 294 insertions, 0 deletions
diff --git a/src/fathom/tbprobe.c b/src/fathom/tbprobe.c
index 5eac014..9cce02d 100644
--- a/src/fathom/tbprobe.c
+++ b/src/fathom/tbprobe.c
@@ -1550,6 +1550,24 @@ static bool init_table(struct BaseEntry *be, const char *str, int type)
be->data[type] = data;
+#ifndef _WIN32
+ if (type == WDL && be->num <= 5) {
+ // 3-5 man WDL files are small (empirically <=20MB each, <=0.4GB total
+ // across every 3-5 man signature -- see CLAUDE.md's EGTB residency
+ // section), so instead of inferring residency later from whether this
+ // header parse has run (a guess that's only reliable because these
+ // files are small enough to end up fully cached anyway), just wire
+ // the whole mapping into RAM now. Once ready[WDL] is true for one of
+ // these signatures, tb_wdl_is_resident()'s "yes" becomes a guarantee
+ // instead of a proxy -- no mincore() needed for this tier at all.
+ if (mlock(data, (size_t)be->mapping[type]) != 0) {
+ perror("mlock");
+ // Not fatal -- this signature's residency answer just reverts to
+ // being an inference (like the 6-man case) instead of a guarantee.
+ }
+ }
+#endif
+
bool split = type != DTZ && (data[4] & 0x01);
if (type == DTM)
be->dtmLossOnly = data[4] & 0x04;
@@ -1900,6 +1918,224 @@ static int probe_table(const Pos *pos, int s, int *success, const int type)
return v;
}
+// Cheap, non-blocking check for whether the WDL table covering a given
+// material signature is already mapped into memory (i.e. init_table()'s
+// one-time mmap()+header-parse has already run for this piece
+// combination, via some earlier probe_table() call this process).
+//
+// This deliberately duplicates probe_table()'s key computation and
+// tbHash[] walk instead of calling it, so that callers can ask "would a
+// probe of this position be fast?" from deep in the search tree without
+// ever taking tbMutex or touching a byte of the underlying file -- it's
+// a handful of popcounts plus one atomic_bool load.
+//
+// Returns:
+// 1 the material signature has a WDL table and it's already resident
+// (mapped + header parsed) -- a probe here should be near-free.
+// 0 the material signature has a WDL table but it hasn't been
+// touched by any probe yet this run -- a probe here will pay the
+// one-time mmap()+parse cost (and is still lazily-mapped, so may
+// also fault in fresh pages from disk for the data itself).
+// -1 no WDL table covers this material signature at all (either it's
+// outside the largest cardinality Fathom found on disk, or it's
+// KvK / otherwise not tabulated).
+//
+// NOTE: a return of 1 only guarantees the table's *header and index
+// metadata* are parsed, not that every compressed data page this
+// specific position's lookup will touch is already in the OS page
+// cache. For the small 3-5 man files that's essentially the same thing
+// in practice (the whole file ends up cached after the first touch);
+// for large 6-man files it's a weaker guarantee. See CLAUDE.md.
+int tb_wdl_is_resident(uint64_t white, uint64_t black, uint64_t queens,
+ uint64_t rooks, uint64_t bishops, uint64_t knights,
+ uint64_t pawns)
+{
+ uint64_t key = popcount(white & queens) * PRIME_WHITE_QUEEN +
+ popcount(white & rooks) * PRIME_WHITE_ROOK +
+ popcount(white & bishops) * PRIME_WHITE_BISHOP +
+ popcount(white & knights) * PRIME_WHITE_KNIGHT +
+ popcount(white & pawns) * PRIME_WHITE_PAWN +
+ popcount(black & queens) * PRIME_BLACK_QUEEN +
+ popcount(black & rooks) * PRIME_BLACK_ROOK +
+ popcount(black & bishops) * PRIME_BLACK_BISHOP +
+ popcount(black & knights) * PRIME_BLACK_KNIGHT +
+ popcount(black & pawns) * PRIME_BLACK_PAWN;
+ if (key == 0ULL) return -1; // KvK: not a tabulated material signature
+
+ int hashIdx = (int)(key >> (64 - TB_HASHBITS));
+ while (tbHash[hashIdx].key && tbHash[hashIdx].key != key)
+ hashIdx = (hashIdx + 1) & ((1 << TB_HASHBITS) - 1);
+ if (!tbHash[hashIdx].ptr ||
+ atomic_load_explicit(&tbHash[hashIdx].error, memory_order_relaxed))
+ return -1;
+
+ struct BaseEntry *be = tbHash[hashIdx].ptr;
+ return atomic_load_explicit(&be->ready[WDL], memory_order_relaxed) ? 1 : 0;
+}
+
+#ifndef _WIN32
+// Page-residency predictor for the 6-man tier, where init_table() can't
+// just mlock() the whole file (some signatures run into the hundreds of
+// MB to low GB -- see CLAUDE.md). Every dereference below is gated by a
+// mincore() check first, so a call to this function never itself blocks
+// on disk I/O: it either confirms every page a real probe_table() lookup
+// would touch is already resident (and, having just read them, returns a
+// verified "yes"), or it stops at the first non-resident page and
+// returns "no" without reading past it.
+//
+// A small direct-mapped cache remembers pages already confirmed resident
+// so repeated probes into the same hot region of a table don't re-pay a
+// mincore() syscall every time. It deliberately only caches "yes"
+// answers -- a page that mincore() reports as not-yet-resident is always
+// re-checked for real next time, so a page that warms up between calls
+// is picked up on its next probe rather than being stuck on a stale "no".
+#define TB_RESIDENCY_PAGE_SIZE 4096u
+#define TB_RESIDENCY_CACHE_BITS 12
+#define TB_RESIDENCY_CACHE_SIZE (1u << TB_RESIDENCY_CACHE_BITS)
+static atomic_uintptr_t g_ResidencyCache[TB_RESIDENCY_CACHE_SIZE];
+
+static size_t _ResidencyCacheSlot(uintptr_t pageAddr)
+{
+ uintptr_t h = (pageAddr >> 12) * 2654435761u;
+ return (size_t)(h & (TB_RESIDENCY_CACHE_SIZE - 1));
+}
+
+static int _PageIsResident(uintptr_t pageAddr)
+{
+ size_t slot = _ResidencyCacheSlot(pageAddr);
+ if (atomic_load_explicit(&g_ResidencyCache[slot], memory_order_relaxed) == pageAddr)
+ return 1;
+
+ unsigned char vec = 0;
+ if (mincore((void *)pageAddr, TB_RESIDENCY_PAGE_SIZE, (char *)&vec) != 0)
+ return 0; // shouldn't happen for a page inside a live mapping; be conservative
+
+ if (vec & 1) {
+ atomic_store_explicit(&g_ResidencyCache[slot], pageAddr, memory_order_relaxed);
+ return 1;
+ }
+ return 0;
+}
+
+static int _RangeIsResident(const void *addr, size_t len)
+{
+ uintptr_t start = (uintptr_t)addr & ~(uintptr_t)(TB_RESIDENCY_PAGE_SIZE - 1);
+ uintptr_t end = ((uintptr_t)addr + len + TB_RESIDENCY_PAGE_SIZE - 1) &
+ ~(uintptr_t)(TB_RESIDENCY_PAGE_SIZE - 1);
+ for (uintptr_t p = start; p < end; p += TB_RESIDENCY_PAGE_SIZE)
+ if (!_PageIsResident(p))
+ return 0;
+ return 1;
+}
+
+// Precise, non-blocking "would a real WDL probe of this exact position
+// hit resident pages only?" check. Unlike tb_wdl_is_resident() (which
+// only knows whether a material signature's header has been parsed at
+// all), this computes the same indexTable/sizeTable/data-block addresses
+// decompress_pairs() would touch for this specific position and confirms
+// each one is actually in the OS page cache before trusting the next.
+//
+// RETURN: 1 if a subsequent real probe is expected to be fully resident
+// (no page fault / disk read), 0 otherwise (table not loaded, material
+// untabulated, or some page on the lookup path isn't resident yet).
+int tb_wdl_probe_is_resident(uint64_t white, uint64_t black, uint64_t kings,
+ uint64_t queens, uint64_t rooks,
+ uint64_t bishops, uint64_t knights,
+ uint64_t pawns, bool turn)
+{
+ Pos pos = { white, black, kings, queens, rooks, bishops, knights, pawns,
+ 0, 0, turn };
+
+ uint64_t key = calc_key(&pos, false);
+ if (key == 0ULL)
+ return 0;
+
+ int hashIdx = (int)(key >> (64 - TB_HASHBITS));
+ while (tbHash[hashIdx].key && tbHash[hashIdx].key != key)
+ hashIdx = (hashIdx + 1) & ((1 << TB_HASHBITS) - 1);
+ if (!tbHash[hashIdx].ptr ||
+ atomic_load_explicit(&tbHash[hashIdx].error, memory_order_relaxed))
+ return 0;
+
+ struct BaseEntry *be = tbHash[hashIdx].ptr;
+ if (!atomic_load_explicit(&be->ready[WDL], memory_order_relaxed))
+ return 0;
+
+ // 3-5 man WDL files are mlock()'d whole at load time (init_table()) --
+ // ready[WDL] already IS the guarantee for this tier, no page check
+ // needed.
+ if (be->num <= 5)
+ return 1;
+
+ bool bside, flip;
+ if (!be->symmetric) {
+ flip = key != be->key;
+ bside = (pos.turn == WHITE) == flip;
+ } else {
+ flip = pos.turn != WHITE;
+ bside = false;
+ }
+
+ struct EncInfo *ei = first_ei(be, WDL);
+ int p[TB_PIECES];
+ size_t idx;
+
+ if (!be->hasPawns) {
+ ei = &ei[bside];
+ for (int i = 0; i < be->num;)
+ i = fill_squares(&pos, ei->pieces, flip, 0, p, i);
+ idx = encode_piece(p, ei, be);
+ } else {
+ int i = fill_squares(&pos, ei->pieces, flip, flip ? 0x38 : 0, p, 0);
+ int t = leading_pawn(p, be, FILE_ENC);
+ ei = &ei[t + 4 * bside];
+ while (i < be->num)
+ i = fill_squares(&pos, ei->pieces, flip, flip ? 0x38 : 0, p, i);
+ idx = encode_pawn_f(p, ei, be);
+ }
+
+ struct PairsData *d = ei->precomp;
+ if (!d->idxBits)
+ return 1; // constant-value table -- no data page to check at all
+
+ uint32_t mainIdx = (uint32_t)(idx >> d->idxBits);
+ uint8_t *idxEntry = d->indexTable + 6 * (size_t)mainIdx;
+ if (!_RangeIsResident(idxEntry, 6))
+ return 0;
+
+ uint32_t block;
+ memcpy(&block, idxEntry, sizeof(block));
+ block = from_le_u32(block);
+ uint16_t idxOffset = *(uint16_t *)(idxEntry + 4);
+
+ int litIdx = (int)((idx & (((size_t)1 << d->idxBits) - 1)) -
+ ((size_t)1 << (d->idxBits - 1)));
+ litIdx += from_le_u16(idxOffset);
+
+ if (litIdx < 0) {
+ while (litIdx < 0) {
+ uint32_t prev = block - 1;
+ if (!_RangeIsResident(&d->sizeTable[prev], sizeof(uint16_t)))
+ return 0;
+ block = prev;
+ litIdx += d->sizeTable[block] + 1;
+ }
+ } else {
+ if (!_RangeIsResident(&d->sizeTable[block], sizeof(uint16_t)))
+ return 0;
+ while (litIdx > d->sizeTable[block]) {
+ litIdx -= d->sizeTable[block] + 1;
+ block++;
+ if (!_RangeIsResident(&d->sizeTable[block], sizeof(uint16_t)))
+ return 0;
+ }
+ }
+
+ uint8_t *dataBlock = d->data + ((size_t)block << d->blockSize);
+ return _RangeIsResident(dataBlock, sizeof(uint32_t) * 2);
+}
+#endif // !_WIN32
+
static int probe_wdl_table(const Pos *pos, int *success)
{
return probe_table(pos, 0, success, WDL);
diff --git a/src/fathom/tbprobe.h b/src/fathom/tbprobe.h
index 82850e7..6f4e5e0 100644
--- a/src/fathom/tbprobe.h
+++ b/src/fathom/tbprobe.h
@@ -181,6 +181,64 @@ bool tb_init(const char *_path);
void tb_free(void);
/*
+ * Cheap, non-blocking check for whether the WDL table covering a given
+ * material signature (per-side piece-type bitboards, no square/turn/ep
+ * info needed) is already mapped into memory from an earlier probe this
+ * process -- i.e. whether a probe of a position with this material is
+ * likely to be low-latency versus pay a first-touch mmap()+parse (and
+ * possibly disk-read) cost. See the routine description in tbprobe.c for
+ * the full contract and caveats.
+ *
+ * PARAMETERS:
+ * - white, black, queens, rooks, bishops, knights, pawns:
+ * Per-side piece-type bitboards (kings/turn/ep are irrelevant to the
+ * material-signature key and are omitted).
+ *
+ * RETURN:
+ * - 1: table exists and is already resident (fast probe expected).
+ * - 0: table exists but hasn't been touched yet this run (first probe
+ * will pay the one-time mmap()+header-parse cost).
+ * - -1: no WDL table covers this material signature at all.
+ */
+int tb_wdl_is_resident(uint64_t white, uint64_t black, uint64_t queens,
+ uint64_t rooks, uint64_t bishops, uint64_t knights,
+ uint64_t pawns);
+
+/*
+ * Precise, non-blocking check for whether a WDL probe of this EXACT
+ * position (not just its material signature) would hit only pages
+ * already resident in the OS page cache. Unlike tb_wdl_is_resident()
+ * above -- which only knows whether the table's header/index metadata
+ * has been parsed at all, a weak proxy for large 6-man files -- this
+ * walks the same indexTable/sizeTable/compressed-data-block addresses a
+ * real probe would touch and confirms each one via mincore() before
+ * trusting the next, so it never blocks on disk I/O itself. Small (3-5
+ * man) WDL files are mlock()'d whole at load time (see init_table() in
+ * tbprobe.c), so for that tier this degenerates to the same
+ * header-parsed check as tb_wdl_is_resident() -- a guarantee, not a
+ * guess, for that tier specifically.
+ *
+ * PARAMETERS:
+ * - white, black, kings, queens, rooks, bishops, knights, pawns:
+ * The current position (bitboards), matching tb_probe_wdl's layout.
+ * - turn: true if White is to move.
+ *
+ * RETURN:
+ * - 1: a real probe here is expected to touch only resident pages.
+ * - 0: table not loaded, material untabulated, or some page on the
+ * lookup path isn't resident yet.
+ *
+ * Not available under _WIN32 (mincore() has no portable equivalent
+ * wired up there yet).
+ */
+#ifndef _WIN32
+int tb_wdl_probe_is_resident(uint64_t white, uint64_t black, uint64_t kings,
+ uint64_t queens, uint64_t rooks,
+ uint64_t bishops, uint64_t knights,
+ uint64_t pawns, bool turn);
+#endif
+
+/*
* Probe the Win-Draw-Loss (WDL) table.
*
* PARAMETERS: