Fable heuristic inline#263
Conversation
Profiles showed call_heuristic as the second-hottest function: every per-suit call rebuilt the full HeuristicContext and re-derived hand_rel, trump state and voidness that MoveGen0/MoveGen123 already knew. The context is now built once per move-generation call and dispatched on the caller's findex. Solve drops from 16.97 to 15.09 ms/hand single-threaded on list1000 (2.9: 16.43); calc from 81.94 to 73.2 ms/hand on list100. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
This PR optimizes heuristic sorting dispatch by hoisting HeuristicContext construction and passing a precomputed dispatch index (findex) into a new call_heuristic(ctx, findex) overload, reducing repeated per-call recomputation in move generation’s hot paths.
Changes:
- Adds a
call_heuristic(const HeuristicContext&, int findex)overload with a direct dispatch switch. - Hoists
HeuristicContextconstruction inMoves::MoveGen0/Moves::MoveGen123viaMoves::make_heuristic_context()and passes precomputedfindex. - Adds a new GoogleTest to verify
findex-based dispatch is equivalent to legacy dispatch for all reachablefindexvalues.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| library/src/moves/moves.hpp | Replaces per-call heuristic invocation with make_heuristic_context() API for hoisted context construction. |
| library/src/moves/moves.cpp | Hoists heuristic context construction and uses new findex overload for hot-path dispatch. |
| library/src/heuristic_sorting/heuristic_sorting.hpp | Makes suit mutable and declares new call_heuristic(ctx, findex) overload. |
| library/src/heuristic_sorting/heuristic_sorting.cpp | Implements findex-based dispatcher and refactors legacy overload to delegate to it. |
| library/tests/heuristic_sorting/dispatch_findex_test.cpp | Adds equivalence tests for legacy vs findex dispatch across reachable indices. |
| library/tests/heuristic_sorting/BUILD.bazel | Registers the new dispatch_findex_test target. |
Clarify that bit 0/1 means a trump winner is available, not merely a trump game, and pin the edge case with a dispatch equivalence test. Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid dereferencing the nullable Moves::trackp member so misuse is a compile-time error instead of undefined behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
An out-of-range findex previously left stale move weights untouched, which could make MergeSort ordering nondeterministic. Co-authored-by: Cursor <cursoragent@cursor.com>
The helper reads lead/curr/trump/suit/move-count members that the constructor leaves uninitialized; set them so the snapshot test avoids UB. Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid ~1MB of per-fixture stack for a zero-initialized table that tests only need by pointer. Co-authored-by: Cursor <cursoragent@cursor.com>
…uristic. Match the legacy dispatcher’s suit-range check before indexing winner[trump] in MoveGen, and drop const_cast by making weighting overloads non-const. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
library/src/moves/moves.cpp:267
- MoveGen123 hoists make_heuristic_context() but does not initialize lastNumMoves/suit beforehand. This makes make_heuristic_context() read indeterminate member values (undefined behavior). Initialize these members before creating the context.
for (int s = 0; s < DDS_SUITS; s++)
trackp->lowest_win[handRel][s] = 0;
numMoves = 0;
int findex;
make_heuristic_context snapshots suit/lastNumMoves/leadSuit; define them in the constructor and reset them in MoveGen0/123 so the hoist does not read indeterminate values. Co-authored-by: Cursor <cursoragent@cursor.com>
Align the behaviour bullet with the mutable HeuristicContext& API and the legacy vs findex dispatch forms. Co-authored-by: Cursor <cursoragent@cursor.com>
The previous 0x3fff-everywhere fixture wrote past MovePlyType::move[14] and failed under ASan. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
library/src/moves/moves.cpp:732
make_heuristic_context()unconditionally snapshotstr.move[1],tr.move[2], andtr.high[...]. For leading-hand generation (MoveGen0) and for second hand in a trick (hand_rel==1), those track slots may not have been populated yet (Init() doesn’t initialize them), so this risks reading indeterminate values (UB) just to populate the snapshot fields.
A safer approach is to snapshot only the trick-card fields that are defined for the current hand_rel (and leave the others as 0, matching the default member initializers).
// Snapshot removed_ranks and minimal trick state into the context to avoid
// direct dependence on the mutable Moves::trackp buffer inside heuristic
// code.
for (int s = 0; s < DDS_SUITS; ++s)
context.removed_ranks[s] = tr.removed_ranks[s];
context.move1_rank = tr.move[1].rank;
context.high1 = tr.high[1];
context.move1_suit = tr.move[1].suit;
context.move2_rank = tr.move[2].rank;
context.move2_suit = tr.move[2].suit;
context.high2 = tr.high[2];
context.lead0_rank = tr.move[0].rank;
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Gate lead/move1/move2 copies on hand_rel so MoveGen0 and early following hands do not read unpopulated TrackType slots. Co-authored-by: Cursor <cursoragent@cursor.com>
Locals in context_for_hand_rel died on return while HeuristicContext kept references to them, leaving dangling pointers in the snapshot. Co-authored-by: Cursor <cursoragent@cursor.com>
| /// @note Prefer the `call_heuristic(context, findex)` overload when the dispatch | ||
| /// index is already known (hot path). Use this overload when callers do | ||
| /// not have `findex` available. | ||
| void call_heuristic(HeuristicContext& context); |
There was a problem hiding this comment.
As mentioned above, we want to refactor any call sites that don't pre-calculate findex. Maybe change this method to return findex to avoid making two calls, one to get a findex and another to sort.
|
This looks promising. Memory serves me correctly 2.9 had an array of pointers to functions which was very hard to understand and test. It looks like There probably is a good reason to not include |
Move generation already passes a precomputed findex everywhere; drop the unused context-only API and the equivalence tests that depended on it. Co-authored-by: Cursor <cursoragent@cursor.com>
Documentation for the method already lives in moves.hpp. Co-authored-by: Cursor <cursoragent@cursor.com>
Fable found this performance improvement by profiling v2.9 and the current develop head. It helps us get closer to v2.9 performance.