diff options
| author | Scott Gasch <[email protected]> | 2026-09-25 08:44:25 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-25 08:44:25 -0700 |
| commit | 9b28080f8b3dca4588cb2aaaf748451f45e3c626 (patch) | |
| tree | 54e0aff5d6ee1520c590c7c04a5ae2f8299e7497 /src/fathom/tbprobe.c | |
| parent | 66740dc89c7351c120c25788d0079055034f4942 (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/tbprobe.c')
| -rw-r--r-- | src/fathom/tbprobe.c | 236 |
1 files changed, 236 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); |
