Skip to content

Add the shape-pattern transposition table and make it the default - #368

Open
tameware wants to merge 10 commits into
dds-bridge:developfrom
tameware:fable/bridge-solver-shape-pattern-transposition
Open

Add the shape-pattern transposition table and make it the default#368
tameware wants to merge 10 commits into
dds-bridge:developfrom
tameware:fable/bridge-solver-shape-pattern-transposition

Conversation

@tameware

@tameware tameware commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add TransTableP, a transposition table organised as suit-length shape → relative-rank patterns, following the cache design of macroxue's bridge-solver. A pattern records only the cards that decided a result (those at or above the lowest winning rank per suit, by owner), so one entry generalises to many positions.
  • Under each shape patterns are unbounded in number, ordered most-general-first and bucketed by the owner of the first relevant suit's top card, so a lookup scans 5 of 17 buckets. Blocks are cache-line aligned and pooled per size class; on reaching the memory maximum the table is cleared rather than harvested.
  • Make TTKind::Pattern (2) the SolverConfig default. DDS_TT_KIND=small|large|pattern overrides it at runtime. C API signatures unchanged (tt_kind doc updated).
  • Update specs/transposition-table.md, specs/solver-context.md, docs/c++_interface.md.

Bridge-solver's subsumption tree was implemented first and profiled; it was a net loss for DDS's access pattern, so the flat generic-first list is what ships.

Results

Identical answers to TransTableL on list100, list1000 and the freak0 deal.

Workload Large Pattern
list1000 calc (per board) 91.3 ms 87.7 ms
list1000 solve (per board) 18.6 ms 18.9 ms
freak0 calc, 8 threads 11.7 s 8.8 s
freak0, 1 thread, large TT 25.3 s 14.3 s
freak0 @ 40 MB 69 s 28 s
freak0 @ 20 MB 1034 s 48 s

Parity on random deals; large wins on void-heavy deals and under memory pressure, where TransTableL's fixed per-shape blocks overflow (61% of adds on freak0) and lookups degrade to ~94-entry linear scans.

Not in this PR

  • .NET SolverConfig still defaults to TTKind.Large and its enum lacks Pattern = 2; the web WASM build pins Small.

Test plan

  • bazelisk test //... (93/93)
  • New white-box tests //library/tests/trans_table:trans_table (trans_table_p_test.cpp)
  • configure_tt_api_test: default kind, Pattern create/switch/resize, env override; dds_c_api_test with tt_kind = 2
  • dtest -s solve/calc on hands/list100.txt, hands/list1000.txt and freak0 with DDS_TT_KIND unset, large, pattern: no differences

tameware and others added 2 commits September 8, 2026 17:20
TransTableP keys positions by suit-length shape and stores, under each
shape, the relative-rank patterns that decided the result (the cards at
or above the lowest winning rank per suit, by owner), following the
cache design of macroxue's bridge-solver. Patterns are ordered most
general first and bucketed by the owner of the first relevant suit's top
card, so a lookup scans only the buckets it can match. Blocks are
cache-line aligned and pooled per size class; when the memory maximum is
reached the table is cleared rather than harvested.

Results are identical to TransTableL. Performance is at parity on random
deals and markedly better on void-heavy deals and under tight memory
limits, where TransTableL's fixed per-shape blocks overflow and lookups
degrade to long linear scans.

TTKind::Pattern (2) is the new SolverConfig default; DDS_TT_KIND=
small|large|pattern overrides it. Specs and C API docs updated.

Co-authored-by: Cursor <cursoragent@cursor.com>
alignas(CacheLine) rounded the 76-byte header up to 128 bytes implicitly,
which MSVC reports as C4324 and /WX turns into an error. Make the padding
an explicit member and static_assert the resulting layout, so the block
header is identical on every compiler.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tameware

tameware commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

See Issue #367

This took Fable a couple of hours to implement. The speedup is small but significant. @zzcgumn's call on whether to merge this. There are other techniques we can borrow from bridge-solver that will likely enhance performance further.

@tameware

tameware commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Multithreaded benchmark:

% python3 python/utilities/src/benchmark.py --repeats 10 --branch develop --branch fable/bridge-solver-shape-pattern-transposition --max_deals 1000
Building dtest from 'develop'...
Building dtest from 'fable/bridge-solver-shape-pattern-transposition'...
Restoring 'benchmark'...
DDS dtest benchmark
===================
baseline:    develop  (/var/folders/12/xtx6dlwd0mdcxkspvmsszsrc0000gn/T/dds-dtest-bin.5wa8015w)
binary 2:    fable/bridge-solver-shape-pattern-transposition  (/var/folders/12/xtx6dlwd0mdcxkspvmsszsrc0000gn/T/dds-dtest-bin.60fc_zf0)
details:     off (summary only)
run order:   interleaved develop, fable/bridge-solver-shape-pattern-transposition
epsilon:     0.5%
hands dir:   /Users/adamw/src/dds/hands
max_deals:   1000
files:       list1000.txt list100.txt list10.txt list1.txt
solvers:     solve calc
git branch:  benchmark
repeats:     10


Summary (avg user ms)
==============================================================================
solver file               develop fable/bridge        rel note
------ ------------- ------------ ------------ ---------- ---------------
solve  list1000.txt          1.92         1.80      0.94x fable/bridge-solver-shape-pattern-transposition faster
solve  list100.txt           2.61         2.33      0.89x fable/bridge-solver-shape-pattern-transposition faster
solve  list10.txt           25.18        23.13      0.92x fable/bridge-solver-shape-pattern-transposition faster
solve  list1.txt             8.00         7.00      0.88x fable/bridge-solver-shape-pattern-transposition faster
calc   list1000.txt          7.10         6.88      0.97x fable/bridge-solver-shape-pattern-transposition faster
calc   list100.txt           7.16         6.82      0.95x fable/bridge-solver-shape-pattern-transposition faster
calc   list10.txt           22.37        20.05      0.90x fable/bridge-solver-shape-pattern-transposition faster
calc   list1.txt            53.60        50.00      0.93x fable/bridge-solver-shape-pattern-transposition faster
------ ------------- ------------ ------------ ---------- ---------------
TOTAL  solve                 2.20         2.05      0.93x fable/bridge-solver-shape-pattern-transposition faster
TOTAL  calc                  7.29         7.03      0.96x fable/bridge-solver-shape-pattern-transposition faster

@tameware

tameware commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Single-threaded benchmark:

% python3 python/utilities/src/benchmark.py --repeats 10 --branch develop --branch fable/bridge-solver-shape-pattern-transposition --bridgesolver ../bridge-solver/solver -s calc --max_deals 100 -- -n 1
Building dtest from 'develop'...
Building dtest from 'fable/bridge-solver-shape-pattern-transposition'...
Restoring 'benchmark'...
Building current-tree dtest for --bridgesolver harness...
DDS dtest benchmark
===================
baseline:    develop  (/var/folders/12/xtx6dlwd0mdcxkspvmsszsrc0000gn/T/dds-dtest-bin.taq0ekva)
binary 2:    fable/bridge-solver-shape-pattern-transposition  (/var/folders/12/xtx6dlwd0mdcxkspvmsszsrc0000gn/T/dds-dtest-bin.oqsg7ec_)
binary 3:    solver  (/var/folders/12/xtx6dlwd0mdcxkspvmsszsrc0000gn/T/dds-dtest-bin.ti537hl1)
details:     off (summary only)
run order:   interleaved develop, fable/bridge-solver-shape-pattern-transposition, solver
hands dir:   /Users/adamw/src/dds/hands
max_deals:   100
files:       list100.txt list10.txt list1.txt
solvers:     calc
git branch:  benchmark
repeats:     10
bridgesolver: ../bridge-solver/solver
dtest args:  -n 1


Summary (avg user ms)
==============================================================================
solver file               develop fable/bridge       solver
------ ------------- ------------ ------------ ------------
calc   list100.txt          83.51        84.08        82.94
calc   list10.txt           76.63        75.25        62.91
calc   list1.txt           131.50       130.50       113.10
------ ------------- ------------ ------------ ------------
TOTAL  calc                 83.32        83.70        81.41

setenv/unsetenv do not exist on MSVC; use _putenv_s there, as args_test
already does.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tameware

tameware commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

"Freak" deal timing:

develop:

% time bazel-bin/utilities/dd_table_for_deal "N:AQT62..Q97.AT832 K85.KJ753..KJ765 9.AT8642.AJ8642. J743.Q9.KT53.Q94"
dd_table_for_deal:
------------------
            AQT62
            -
            Q97
            AT832
J743                    K85
Q9                      KJ753
KT53                    -
Q94                     KJ765
            9
            AT8642
            AJ8642
            -

      North South East  West
   NT     7     7     5     6
    S     8     8     5     5
    H     8     8     5     5
    D    11    11     2     2
    C     6     6     6     7

Par: NS 5D = 400
bazel-bin/utilities/dd_table_for_deal   27.56s user 0.12s system 228% cpu 12.098 total

This branch:

% time bazel-bin/utilities/dd_table_for_deal "N:AQT62..Q97.AT832 K85.KJ753..KJ765 9.AT8642.AJ8642. J743.Q9.KT53.Q94"
…
bazel-bin/utilities/dd_table_for_deal   15.06s user 0.09s system 168% cpu 8.987 total

bridge-solver:

% time ../bridge-solver/solver -f deals/freak/deal.0
Input file not found: 'deals/freak/deal.0'.
../bridge-solver/solver -f deals/freak/deal.0  0.00s user 0.00s system 51% cpu 0.006 total
(base) adamw@MacBook-Pro dds % time ../bridge-solver/solver -f ../bridge-solver/deals/freak/deal.0
                          ♠ AQT62 ♥ - ♦ Q97 ♣ AT832
  ♠ J743 ♥ Q9 ♦ KT53 ♣ Q94                       ♠ K85 ♥ KJ753 ♦ - ♣ KJ765
                          ♠ 9 ♥ AT8642 ♦ AJ8642 ♣ -
N  7  7  6  5  4.76 s 261344.0 M
S  8  8  5  5  5.11 s 261376.0 M
H  8  8  5  5  6.00 s 261520.0 M
D 11 11  2  2  6.07 s 261520.0 M
C  6  6  7  6  7.63 s 262000.0 M
../bridge-solver/solver -f ../bridge-solver/deals/freak/deal.0  7.57s user 0.05s system 99% cpu 7.634 total

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Duplicate inserts can trigger unnecessary table resets, and lowering the hard memory limit is not immediately enforced.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds the shape-pattern transposition table, makes it the default, and exposes configuration through existing APIs.

Changes:

  • Implements pooled, shape-keyed relative-rank pattern caching.
  • Adds comprehensive table and API tests.
  • Updates defaults, build targets, and documentation.
File summaries
File Description
specs/transposition-table.md Documents Pattern table behavior.
specs/solver-context.md Documents the new default.
library/tests/trans_table/trans_table_p_test.cpp Tests matching, storage, memory, and parity.
library/tests/trans_table/BUILD.bazel Registers Pattern table tests.
library/tests/system/configure_tt_api_test.cpp Tests configuration and environment overrides.
library/tests/dds_c_api_test.cpp Tests Pattern through the C API.
library/src/trans_table/trans_table_p.hpp Declares TransTableP.
library/src/trans_table/trans_table_p.cpp Implements pattern caching and memory management.
library/src/trans_table/BUILD.bazel Adds Pattern sources to build targets.
library/src/solver_context/solver_context.hpp Adds TTKind::Pattern and changes the default.
library/src/solver_context/solver_context.cpp Creates and configures Pattern tables.
library/src/api/dds_c_api.h Documents Pattern’s C ABI value.
docs/c++_interface.md Updates C++ configuration documentation.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread library/src/trans_table/trans_table_p.cpp
Comment thread library/src/trans_table/trans_table_p.cpp Outdated
Comment thread specs/transposition-table.md Outdated
set_memory_maximum() on a live TransTableP that is already above the new
limit now clears the table immediately instead of leaving it over budget
until the next block allocation; inserts into blocks with spare capacity
never consult the budget, so without this a lowered hard cap could go
unenforced indefinitely.

add() now searches the bucket for an identical pattern before reserving
capacity. Re-adding an existing pattern to a full block used to double
the block, or at the memory limit clear the whole table, for an update
that needed no allocation.

Spec: distinguish ordinary resets, which pool the pattern blocks, from
MemoryExhausted resets, which also free the pool.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Memory-exhaustion and teardown paths retain storage and may allocate beyond the configured hard cap.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

library/src/trans_table/trans_table_p.cpp:155

  • return_all_memory() leaves substantial dynamic storage behind: ownership_ keeps its 8,192-entry capacity, and free_spare_trees() only clears each pointer vector without releasing its backing allocation. This violates the base-class contract that all structures are deallocated and leaves memory_in_use() at roughly 384 KiB even after the call. Release these vector capacities as well.
auto TransTableP::return_all_memory() -> void
{
    release_trees();
    free_spare_trees();
    std::vector<ShapeSlot>().swap(shapes_);
}
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread library/src/trans_table/trans_table_p.cpp Outdated
…own.

A MemoryExhausted reset used to push every active block into the spare
pool, which may allocate, only to delete the pool immediately. It now
deletes the blocks outright, so the over-budget recovery path never
allocates. make_tt() uses the same path.

return_all_memory() now also drops the ownership table and the pool
vectors' capacity, so memory_in_use() is zero afterwards as the base
contract requires; init() rebuilds the ownership table per deal anyway.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Shape-table allocation can transiently exceed the documented hard memory cap, and environment tests leak process state.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

library/tests/system/configure_tt_api_test.cpp:54

  • This permanently removes a caller-provided DDS_TT_KIND, so subsequent tests in this executable no longer observe the environment they were launched with and become order-dependent. Use a guard that captures and restores the original value (as args_test.cpp:98 does); ScopedEnv above must likewise restore the previous value rather than always unsetting it.

library/src/trans_table/trans_table_p.cpp:440

  • This growth check budgets only the final replacement table, but fresh is allocated while the old shapes_ storage is still live. A rehash therefore exceeds the advertised hard cap by the entire old table and can fail under the memory pressure the cap is meant to control. Include current dynamic usage plus the new allocation in the check so the table resets when there is insufficient peak headroom.
    if (new_size * sizeof(ShapeSlot) + tree_bytes_ > maximum_bytes_) {
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread library/src/trans_table/trans_table_p.cpp Outdated
reset_memory() now frees the old shape table before allocating the
fresh one, and grow_shapes() budgets the peak of old and new tables
together, so neither path allocates beyond the configured maximum.

configure_tt_api_test's ScopedEnv restores the previous value of the
variable (or its absence) instead of always unsetting it, and the
default-kind test uses it rather than unsetting DDS_TT_KIND for the rest
of the process.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The owning table remains implicitly copyable, and one allocation-failure path leaks a newly allocated tree.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

library/src/trans_table/trans_table_p.cpp:419

  • release_tree(old) can allocate while growing the spare-vector and throw after fresh has been allocated but before it is attached to slot. On that path fresh is unreachable, so its aligned allocation leaks and tree_bytes_ remains permanently inflated. Clean up fresh if pooling the old block fails (or hold it in an RAII owner until commit).
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread library/src/trans_table/trans_table_p.hpp
The table owns raw pattern blocks, so the implicit copy operations would
alias and later double-free them; delete them.

reserve_one_more() now attaches the grown block to its slot before
pooling the old one, and deletes the old block if pooling throws, so an
allocation failure at that point leaks nothing and keeps the byte
accounting exact.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Spare-pool bookkeeping is excluded from hard-limit accounting, allowing retained allocation to exceed the configured maximum.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

library/src/trans_table/trans_table_p.cpp:174

  • The hard-cap calculation omits the backing allocations owned by spare_trees_. Those vectors grow when old blocks are pooled, and free_spare_trees() only calls clear(), so a large pooled table can retain megabytes of pointer storage after a MemoryExhausted reset or a lower maximum while dynamic_bytes() reports that the cap is satisfied. Include the spare-vector capacities in memory accounting and release or budget their capacity on hard-cap paths.
    library/src/trans_table/trans_table_p.cpp:26
  • This says equal-weight patterns are newest-first, but insertion walks past all equal-weight nonmatches before inserting, so they remain oldest-first. Update the comment (or change insertion ordering) so the documented lookup order matches the implementation.
  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

…g note.

dynamic_bytes() now includes the capacity of the spare-block pointer
vectors, and free_spare_trees() (over-budget and teardown paths) releases
that capacity as well, so nothing the table retains escapes the hard cap.

Patterns of equal weight are scanned oldest first, as the insertion code
has always done; the file comment claimed newest first. A test now pins
the actual order.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tameware

Copy link
Copy Markdown
Collaborator Author

Addressed the two suppressed notes from the last review in 0338f36: dynamic_bytes() now counts the spare-pool pointer vectors' capacity and free_spare_trees() releases it on the hard-cap/teardown paths (test: PooledBlockPointerStorageCountsTowardsMemoryInUse); the file comment now says oldest-first among equal-weight patterns, matching the code, pinned by AmongEquallyGenericPatternsTheOlderIsTriedFirst.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Pool metadata growth can violate the configured hard memory cap.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread library/src/trans_table/trans_table_p.cpp Outdated
release_tree() now checks, when the pointer vector would have to grow,
that the growth fits under the hard cap; otherwise the block is returned
to the allocator instead of pooled. With the growth reserved up front,
push_back cannot throw.

A strict-cap test asserts memory_in_use() <= maximum after every add on
a deep, block-heavy workload with no slack for pool bookkeeping.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Environment-resolved kind handling is inconsistent during reconfiguration, and the base transposition-table documentation remains outdated.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

library/src/solver_context/solver_context.cpp:290

  • Compare against the environment-resolved kind. With DDS_TT_KIND=pattern, calling configure_tt(Small, ...) currently destroys and recreates the cache even though the effective kind remains Pattern; if the environment changes to a new override while the configured kind equals the current table, this branch instead leaves the old implementation in place. Resolving the requested kind here keeps configure_tt consistent with the documented override and recreation semantics.
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread library/src/trans_table/trans_table_p.hpp
…leP.

configure_tt() now resolves the DDS_TT_KIND override before deciding
whether to recreate the table, as creation does, so a request that leaves
the effective kind unchanged resizes in place and a changed override
recreates even when the configured kind is the same.

The TransTable base doxygen now lists all three implementations and
their memory strategies.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants