Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
17c2804
Add the shape-pattern transposition table and make it the default.
tameware Sep 8, 2026
d69ca37
Spell out PatternTree's cache-line padding to silence MSVC C4324.
tameware Sep 9, 2026
3e508ac
Set environment variables portably in configure_tt_api_test.
tameware Sep 9, 2026
0e848b5
Enforce a lowered TT maximum at once and tighten before reserving.
tameware Sep 10, 2026
a418a14
Free TT blocks directly on exhaustion and release everything on teard…
tameware Sep 10, 2026
55af56c
Keep TT resets and rehashes within the hard cap; restore env in tests.
tameware Sep 10, 2026
4b6ed69
Make TransTableP non-copyable and leak-free when pooling throws.
tameware Sep 10, 2026
0338f36
Budget the TT pool's pointer storage and fix the equal-weight orderin…
tameware Sep 10, 2026
2dec1fe
Budget the TT pool's pointer growth before pooling a block.
tameware Sep 10, 2026
b640aea
Compare configure_tt against the effective TT kind; document TransTab…
tameware Sep 10, 2026
eb56730
Isolate explicit-kind TT tests from an ambient DDS_TT_KIND override.
tameware Sep 10, 2026
58552a7
Trim the TransTableP memory tests so the suite fits the sanitizer bud…
tameware Sep 10, 2026
cd39294
Indent the new solver_context and configure_tt_api_test code with 4 s…
tameware Sep 11, 2026
bb287b3
Make a TransTableP without a deal inert, and document least_win as a …
tameware Sep 12, 2026
985defd
Keep the whole-suit least_win when a 13/12-relevant pattern merges; a…
tameware Sep 12, 2026
adf9882
Forget remembered lookup shapes on init/make_tt/reset; budget the who…
tameware Sep 12, 2026
3f4aa11
Honour a maximum set without a default; card-aware P dump; vacate sha…
tameware Sep 12, 2026
c8ceeea
Spec: DDS_TT_DEFAULT_MB applies at table creation only, not on in-pla…
tameware Sep 12, 2026
c25111d
Docs: set_memory_default caveat for TransTableP; DDS_TT_LIMIT_MB is f…
tameware Sep 12, 2026
c79a53d
Comment: shape_key relies on the trick-boundary precondition shared w…
tameware Sep 12, 2026
3819518
Keep a maximum-only TT configuration's cap on lazy creation; qualify …
tameware Sep 12, 2026
5490751
Docs: set_memory_default is honoured only by L, 0 is unsupported; sco…
tameware Sep 12, 2026
c996b81
Resolve unset limits in configure_tt like creation; re-init a recreat…
tameware Sep 12, 2026
5186749
Pin that TT entries survive init() for similar deals and still hinge …
tameware Sep 12, 2026
47536f5
Spec: drop the stale claim that the header calls 0 unlimited
tameware Sep 12, 2026
6cb62ec
Docs: reset_memory contract notes the MemoryExhausted exception on Tr…
tameware Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/c++_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Primary entry points:

Fields:

- `tt_kind_`: `TTKind::Small` or `TTKind::Large`
- `tt_kind_`: `TTKind::Pattern` (default), `TTKind::Large` or `TTKind::Small`
- `tt_mem_default_mb_`: default TT memory in MB
- `tt_mem_maximum_mb_`: maximum TT memory in MB

Expand Down
3 changes: 2 additions & 1 deletion library/src/api/dds_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ DLLEXPORT int dds_c_calc_par_pbn(DDS_C_SOLVER_CTX ctx,
is decomposed into scalars rather than mirrored as a struct: passing a struct
by value is exactly the ABI question this shim exists to avoid, and a mirror
type would be a second definition to keep in sync. tt_kind: 0 = Small,
1 = Large (matching enum class TTKind). Returns NULL on failure. */
1 = Large, 2 = Pattern (matching enum class TTKind). Returns NULL on
failure. */
DLLEXPORT DDS_C_SOLVER_CTX dds_c_create_solvercontext(int tt_kind,
int def_mb, int max_mb);

Expand Down
92 changes: 69 additions & 23 deletions library/src/solver_context/solver_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <memory>
Expand All @@ -13,11 +14,63 @@
#include <api/dds_data_types.hpp>
//#include <api/dds_api.hpp>
#include <trans_table/trans_table_l.hpp>
#include <trans_table/trans_table_p.hpp>
#include <trans_table/trans_table_s.hpp>
#include <utility/debug.h>

namespace {

/// Optional DDS_TT_KIND=small|large|pattern override of the configured kind.
Comment thread
tameware marked this conversation as resolved.
auto tt_kind_from_environment(TTKind configured) -> TTKind
{
const char* s = std::getenv("DDS_TT_KIND");
if (s == nullptr) return configured;
const std::string value(s);
if (value == "small") return TTKind::Small;
if (value == "large") return TTKind::Large;
if (value == "pattern") return TTKind::Pattern;
return configured;
}

auto tt_kind_of(const TransTable* tt) -> TTKind
{
if (dynamic_cast<const TransTableS*>(tt) != nullptr) return TTKind::Small;
if (dynamic_cast<const TransTableP*>(tt) != nullptr) return TTKind::Pattern;
return TTKind::Large;
}

auto tt_kind_letter(TTKind kind) -> char
{
switch (kind) {
case TTKind::Small: return 'S';
case TTKind::Pattern: return 'P';
case TTKind::Large: break;
}
return 'L';
}

auto make_trans_table(TTKind kind) -> std::unique_ptr<TransTable>
{
switch (kind) {
case TTKind::Small: return std::make_unique<TransTableS>();
case TTKind::Pattern: return std::make_unique<TransTableP>();
case TTKind::Large: break;
}
return std::make_unique<TransTableL>();
}

/// Replaces non-positive limits with the built-in THREADMEM_* values, one at
/// a time: an unset maximum gets the built-in limit, and an unset default gets
/// the built-in default capped by the (possibly explicit) maximum, so that a
/// maximum-only configuration keeps its cap.
auto fill_unset_limits(const TTKind kind, int& defMB, int& maxMB) -> void
{
const int builtin_def = kind == TTKind::Small ? THREADMEM_SMALL_DEF_MB : THREADMEM_LARGE_DEF_MB;
const int builtin_max = kind == TTKind::Small ? THREADMEM_SMALL_MAX_MB : THREADMEM_LARGE_MAX_MB;
if (maxMB <= 0) maxMB = builtin_max;
if (defMB <= 0) defMB = std::min(builtin_def, maxMB);
}

#if defined(DDS_TOP_LEVEL) || defined(DDS_AB_STATS) || defined(DDS_AB_HITS) || \
defined(DDS_TT_STATS) || defined(DDS_TIMING) || defined(DDS_MOVES)
std::string next_debug_file_suffix()
Expand Down Expand Up @@ -68,20 +121,11 @@ auto SolverContext::trans_table() const -> TransTable*
auto SolverContext::SearchContext::trans_table() -> TransTable* {
if (tt_) return tt_.get();
// Require owner (for config and utilities). If missing, fall back
// to Large with built-in defaults.
TTKind kind = (owner_ ? owner_->config().tt_kind_ : TTKind::Large);
// to the SolverConfig default with built-in memory limits.
TTKind kind = tt_kind_from_environment(owner_ ? owner_->config().tt_kind_ : SolverConfig{}.tt_kind_);
int defMB = (owner_ ? owner_->config().tt_mem_default_mb_ : 0);
int maxMB = (owner_ ? owner_->config().tt_mem_maximum_mb_ : 0);
// Final fallback to THREADMEM_* constants
if (defMB <= 0 || maxMB <= 0) {
if (kind == TTKind::Small) {
defMB = THREADMEM_SMALL_DEF_MB;
maxMB = THREADMEM_SMALL_MAX_MB;
} else {
defMB = THREADMEM_LARGE_DEF_MB;
maxMB = THREADMEM_LARGE_MAX_MB;
}
}
fill_unset_limits(kind, defMB, maxMB);
// Optional environment overrides
if (const char* s = std::getenv("DDS_TT_DEFAULT_MB")) {
int v = std::atoi(s);
Expand All @@ -93,19 +137,15 @@ auto SolverContext::SearchContext::trans_table() -> TransTable* {
}
if (maxMB < defMB) maxMB = defMB;

// Create appropriate concrete table
if (kind == TTKind::Small)
tt_ = std::unique_ptr<TransTable>(new TransTableS());
else
tt_ = std::unique_ptr<TransTable>(new TransTableL());
tt_ = make_trans_table(kind);

tt_->set_memory_default(defMB);
tt_->set_memory_maximum(maxMB);
tt_->make_tt();

#ifdef DDS_UTILITIES_LOG
{
const char kch = (kind == TTKind::Small ? 'S' : 'L');
const char kch = tt_kind_letter(kind);
char buf[96];
std::snprintf(buf, sizeof(buf), "tt:create|%c|%d|%d", kch, defMB, maxMB);
if (owner_) owner_->utilities().log_append(std::string(buf));
Expand All @@ -120,7 +160,7 @@ auto SolverContext::SearchContext::trans_table() -> TransTable* {
if (const char* dbg = std::getenv("DDS_DEBUG_TT_CREATE")) {
if (*dbg) {
std::cerr << "[DDS] TT create: kind="
<< (kind == TTKind::Small ? 'S' : 'L')
<< tt_kind_letter(kind)
<< " defMB=" << defMB
<< " maxMB=" << maxMB
<< std::endl;
Expand All @@ -146,6 +186,10 @@ auto SolverContext::dispose_trans_table() const -> void
#endif
// Dispose the member-owned TT (if any)
const_cast<SolverContext*>(this)->search_.dispose_trans_table();
// A replacement table has not seen the current deal. Forget the deal the
// thread remembers so the next solve treats it as new and runs
// SetDealTables(), which init()s the table, even for the same cards.
if (thr_) std::memset(thr_->suit, 0, sizeof(thr_->suit));
}

// Defaulted destructor defined out-of-line so destruction of the
Expand Down Expand Up @@ -235,6 +279,9 @@ auto SolverContext::resize_tt(int defMB, int maxMB) const -> void

auto SolverContext::configure_tt(TTKind kind, int defMB, int maxMB) -> void
{
// Unset limits resolve exactly as they would on lazy creation, so that an
// in-place resize never hands a live table a zero maximum.
fill_unset_limits(tt_kind_from_environment(kind), defMB, maxMB);
// Apply environment limit if present to preserve existing behavior.
if (const char* s = std::getenv("DDS_TT_LIMIT_MB")) {
int v = std::atoi(s);
Expand All @@ -250,10 +297,9 @@ auto SolverContext::configure_tt(TTKind kind, int defMB, int maxMB) -> void
auto* tt = search_.maybe_trans_table();
if (!tt) return; // Nothing to apply now; will take effect on lazy creation.

// If kind changes, dispose and recreate now to ensure effect is applied.
bool is_small = (dynamic_cast<TransTableS*>(tt) != nullptr);
TTKind current_kind = is_small ? TTKind::Small : TTKind::Large;
if (current_kind != kind) {
// If the effective kind (environment override included, as at creation)
// changes, dispose and recreate now to ensure effect is applied.
if (tt_kind_of(tt) != tt_kind_from_environment(kind)) {
dispose_trans_table();
// Force immediate creation with new config to keep behavior explicit.
(void)trans_table();
Expand Down
9 changes: 7 additions & 2 deletions library/src/solver_context/solver_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@

// Minimal configuration scaffold for future expansion.
// TT configuration without depending on Memory headers.
enum class TTKind { Small, Large };
/// Transposition table implementation:
/// - Small: pool-based, low memory (TransTableS)
/// - Large: paged, flat per-shape entry lists (TransTableL)
/// - Pattern: shape → generality-ordered relative-rank patterns (TransTableP)
/// The integer values are part of the C ABI (dds_c_create_solvercontext).
enum class TTKind { Small = 0, Large = 1, Pattern = 2 };

/**
* @brief Configuration options for SolverContext instances.
Expand All @@ -31,7 +36,7 @@ enum class TTKind { Small, Large };
*/
struct SolverConfig
{
TTKind tt_kind_ = TTKind::Large;
TTKind tt_kind_ = TTKind::Pattern;
int tt_mem_default_mb_ = 0;
int tt_mem_maximum_mb_ = 0;
};
Expand Down
4 changes: 4 additions & 0 deletions library/src/trans_table/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ cc_library(
name = "trans_table",
srcs = [
"trans_table_l.cpp",
"trans_table_p.cpp",
Comment thread
tameware marked this conversation as resolved.
"trans_table_s.cpp",
],
hdrs = [
"trans_table.hpp",
"trans_table_l.hpp",
"trans_table_p.hpp",
"trans_table_s.hpp",
],
visibility = ["//visibility:public"],
Expand All @@ -28,11 +30,13 @@ cc_library(
name = "testable_trans_table",
srcs = [
"trans_table_l.cpp",
"trans_table_p.cpp",
"trans_table_s.cpp",
],
hdrs = [
"trans_table.hpp",
"trans_table_l.hpp",
"trans_table_p.hpp",
"trans_table_s.hpp",
],
copts = DDS_CPPOPTS,
Expand Down
47 changes: 32 additions & 15 deletions library/src/trans_table/trans_table.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
*/

/*
This is the parent class of TransTableS and TransTableL.
Those two are different implementations. The S version has a
much smaller memory and a somewhat slower execution time.
This is the parent class of TransTableP, TransTableL and TransTableS.
They are different implementations of the same interface: P (the
default) stores shape-keyed relative-rank patterns, L is the paged
table with harvesting, and S has a much smaller memory footprint and a
somewhat slower execution time.
*/

#pragma once
Expand Down Expand Up @@ -55,7 +57,11 @@ struct NodeCards // 8 bytes
char lower_bound; ///< Minimum tricks for side to move at this node (0-13)
char best_move_suit; ///< Optimal suit index (0=S, 1=H, 2=D, 3=C; matches card_suit)
char best_move_rank; ///< Absolute rank (2-14 for 2-A), 0 used as sentinel
char least_win[DDS_SUITS]; ///< Encoded lowest winning rank (0-13), used as 15 - least_win
char least_win[DDS_SUITS]; ///< Per suit, the number (0-13) of remaining cards at or
///< above the lowest winning rank; ab_search feeds it to
///< win_ranks[aggr][least_win] to recover those cards. Not
///< an absolute rank: 15 - least_win is the *relative* rank
///< of the lowest such card, as the dump routines print it.
};

#ifdef _MSC_VER
Expand All @@ -78,14 +84,17 @@ struct NodeCards // 8 bytes
///
/// TransTable defines the interface for managing cached positions during
/// double dummy analysis. The transposition table stores previously computed
/// results to avoid redundant search work. Two implementations are provided:
/// - TransTableS: Memory-efficient small transposition table
/// results to avoid redundant search work. Three implementations are provided:
/// - TransTableP: Shape-keyed relative-rank patterns (the default)
/// - TransTableL: Full-featured large transposition table with paging
/// - TransTableS: Memory-efficient small transposition table
///
/// \par Memory Management Strategy
/// Implementations use different memory strategies. TransTableS uses a pool-based
/// approach with malloc/calloc, while TransTableL uses paged memory with
/// harvesting. Both support configurable memory limits and graceful degradation.
/// Implementations use different memory strategies. TransTableP grows on
/// demand and clears itself when the next allocation would exceed the maximum,
/// TransTableL uses paged memory with harvesting, and TransTableS uses a
/// pool-based approach with malloc/calloc. All support configurable memory
/// limits and graceful degradation.
///
/// \par Thread Safety
/// Not thread-safe. The transposition table must be accessed from a single
Expand Down Expand Up @@ -116,11 +125,15 @@ class TransTable

/// \brief Set the default (soft) memory limit in megabytes.
///
/// The table will try to stay below this limit but may exceed it slightly
/// during search. When the limit is exceeded, the table may invoke cleanup
/// strategies like harvesting (in TransTableL).
/// Only TransTableL treats this as a soft limit: it tries to stay below it
/// but may exceed it slightly during search, harvesting when it does.
/// TransTableS ignores the value (a no-op; only the maximum is enforced).
/// TransTableP has no soft limit either: the value only floors the hard
/// maximum at make_tt(), and 0 (unset) is ignored there.
///
/// \param megabytes Desired soft memory limit in MB (0 = unlimited)
/// \param megabytes Desired soft memory limit in MB. 0 is not supported as
/// "unlimited": TransTableL would free every pooled page on the next
/// reset; pass a positive value.
virtual auto set_memory_default(int megabytes) -> void = 0;

/// \brief Set the maximum (hard) memory limit in megabytes.
Expand All @@ -141,8 +154,12 @@ class TransTable

/// \brief Clear the transposition table and reset memory/statistics.
///
/// Removes all cached positions and resets internal statistics. The memory
/// structures are retained for reuse.
/// Removes all cached positions and bumps the per-reason reset counters
/// (other statistics accumulate across resets). The memory structures are
/// retained for reuse, with one exception: on TransTableP a
/// ResetReason::MemoryExhausted reset returns the pattern blocks (in use
/// and pooled) to the allocator, since pooling them could itself allocate
/// while over budget; the table then regrows on demand.
///
/// \param reason The reason this reset was triggered
virtual auto reset_memory(ResetReason reason) -> void = 0;
Expand Down
Loading