Skip to content

Performance Analysis: Potential Optimization Opportunities in ab_search.cpp and quick_tricks.cpp #230

Description

@wopdevries

Summary

Using static analysis and profiling best practices, I've identified 10 potential performance bottlenecks in the hot-path code (ab_search.cpp and quick_tricks.cpp). These represent optimization opportunities that could improve solver throughput by 5-30% depending on implementation.

This issue provides:

  1. ✅ Detailed identification of each bottleneck with exact code locations
  2. Quick-win optimizations with ready-to-use code examples
  3. ✅ A structured 4-week profiling plan with shell commands
  4. ✅ Questions for the maintainer team

🎯 Quick Wins (5-15% Estimated Gain)

1. Extract Duplicated TT Lookup Logic

Location: ab_search.cpp lines 207-247 and 317-357
Issue: Identical transposition table lookup code duplicated at different depths
Effort: Low | Gain: 5-10%

// Extract to helper function
static inline std::optional<bool> tt_lookup(
    Pos* posPoint, int depth, int tricks, int hand,
    int target, SolverContext& ctx)
{
  int limit;
  if (ctx.search().node_type_store(0) == MAXNODE)
    limit = target - posPoint->tricks_max - 1;
  else
    limit = tricks - (target - posPoint->tricks_max - 1);

  bool lowerFlag;
  TIMER_START(TIMER_NO_LOOKUP, depth);
  NodeCards const* cardsP = ctx.trans_table()->lookup(
      tricks, hand, posPoint->aggr, posPoint->hand_dist,
      limit, lowerFlag);
  TIMER_END(TIMER_NO_LOOKUP, depth);

  if (!cardsP) return std::nullopt;

  for (int ss = 0; ss < DDS_SUITS; ss++)
    posPoint->win_ranks[depth][ss] =
        win_ranks[posPoint->aggr[ss]]
                 [static_cast<unsigned char>(cardsP->least_win[ss])];

  if (cardsP->best_move_rank != 0) {
    ctx.search().best_move_tt(depth).suit =
        static_cast<unsigned char>(cardsP->best_move_suit);
    ctx.search().best_move_tt(depth).rank =
        static_cast<unsigned char>(cardsP->best_move_rank);
  }

  bool scoreFlag =
      (ctx.search().node_type_store(0) == MAXNODE ? lowerFlag : !lowerFlag);
  AB_COUNT(AB_MAIN_LOOKUP, scoreFlag, depth);
  return scoreFlag;
}

2. Add Branch Prediction Hints

Location: quick_tricks.cpp lines 145-666 and ab_search.cpp cutoff checks
Issue: Multiple unpredictable branches with no compiler hints
Effort: Low | Gain: 3-8%

// Add C++20 attributes
if (posPoint->tricks_max >= target) [[likely]] {
    return true;
} else if (posPoint->tricks_max + tricks + 1 < target) [[unlikely]] {
    return false;
}

for (int s = 0; s < DDS_SUITS; s++) {
    if (!opps && (countPart == 0)) [[unlikely]] {
        // Rare case
    } else [[likely]] {
        // Common case
    }
}

3. Cache Hot Values

Location: Throughout both files
Issue: (depth + 3) >> 2 computed repeatedly; bit patterns recomputed
Effort: Low | Gain: 2-5%

// Cache at function entry
bool ab_search_1_ctx(Pos* posPoint, const int target, const int depth, SolverContext& ctx) {
    const int tricks = (depth + 3) >> 2;  // Compute once
    ThreadData* thrp = ctx.thread_ptr();   // Cache pointer
    
    // Reuse throughout function
    ctx.move_gen().move_gen_123(tricks, 1, *posPoint);
    // ... no recalculation needed
}

4. Optimize Memory Alignment

Location: trans_table.hpp line 52 - NodeCards structure
Issue: 8-byte entries may cause cache line conflicts
Effort: Low | Gain: 5-15% (if contention is high)

struct NodeCards {
    char upper_bound;
    char lower_bound;
    char best_move_suit;
    char best_move_rank;
    char least_win[DDS_SUITS];
    char _padding[56];  // Align to 64-byte cache line
} __attribute__((aligned(64)));

Update 2026-07-19: This suggestion is incorrect.
NodeCards is deliberately 8 bytes to maximize TT density — padding to 64 bytes
would increase memory usage 8× and hurt cache hit rate, not help it.
There is no false sharing issue since the TT is single-threaded per context.
Per @tameware: the correct direction is Stockfish-style — keep NodeCards at 8 bytes
and check WinMatch/block layout for awkward cache line straddling without multiplying per-entry size.``

5. Reduce Pointer Dereferencing

Location: ab_search.cpp make/undo functions
Issue: Multiple indirections accumulate with deep recursion
Effort: Low | Gain: 2-5%

void make_1(Pos* posPoint, const int depth, MoveType const* mply) {
    Pos& pos = *posPoint;  // Single indirection
    
    pos.rank_in_suit[h][s] &= (~bit_map_rank[r]);
    pos.aggr[s] ^= bit_map_rank[r];
    // ... reuse pos reference
}

📊 All 10 Issues (Detailed Breakdown)

# Issue Location Impact Effort Est. Gain
1 Duplicated TT Lookup ab_search.cpp 207-247, 317-357 HIGH Low 5-10%
2 Excessive Loop Nesting quick_tricks.cpp 229-666 MEDIUM-HIGH Medium 5-15%
3 Repeated Bit Operations Both files MEDIUM Low 2-5%
4 Memory Alignment trans_table.hpp 52 MEDIUM Low 5-15%
5 Recomputed Arithmetic Both files MEDIUM Low 2-3%
6 Pointer Dereferencing Both files MEDIUM Low 2-5%
7 Branch Misprediction Both files MEDIUM Low 3-8%
8 Memory Fragmentation TransTable malloc/calloc LOW-MEDIUM Medium 3-10%
9 Missing Compiler Hints Throughout LOW-MEDIUM Low 2-5%
10 TLS Overhead ab_search.cpp 93, 195, 496 LOW Low 1-2%

📋 4-Week Profiling Plan

Week 1: Baseline Metrics (Linux)

# Build with profiling symbols
bazelisk build --copt=-fno-omit-frame-pointer -c opt //library/tests:dtest

# Generate flame graph (requires FlameGraph: github.com/brendangregg/FlameGraph)
cd bazel-bin/library/tests
perf record -F 99 -g ./dtest -f ~/dds/hands/list1000.txt -s solve -n 1
perf script > ~/dds/out.perf
~/FlameGraph/stackcollapse-perf.pl ~/dds/out.perf > ~/dds/out.folded
~/FlameGraph/flamegraph.pl ~/dds/out.folded > ~/dds/dds_flame.svg

Note: requires kernel.perf_event_paranoid=1 (sudo sysctl kernel.perf_event_paranoid=1)

Week 2: Event Counters & Profiling (Linux)

perf stat -e cycles,instructions,branch-instructions,branch-misses \
  ./bazel-bin/library/tests/dtest -f hands/list1000.txt -s solve -n 1

Results (Intel i7-4770, Z87, 1000 hands): branch miss rate 4.32%, IPC 1.57

Week 3: Data Structure & Memory Analysis (Linux)

valgrind --tool=cachegrind \
  ./bazel-bin/library/tests/dtest -f hands/list1000.txt -s solve -n 1
cg_annotate cachegrind.out.* > cache_report.txt

Deliverable: Per-function cache behavior, memory access patterns

Week 4+: Implementation & Validation

  • Implement 1-2 high-confidence optimizations
  • Measure improvement vs. baseline
  • Prepare pull request with benchmark results

🔍 Questions for Maintainers

  1. Prior Profiling: Has profiling been done on the current codebase? Which functions dominate CPU time?
  2. TT Performance: What transposition table hit rates do you observe in practice (target: >50%)?
  3. Known Bottlenecks: Are there specific board configurations or scenarios known to be slow?
  4. Contribution Welcome: Would optimization PRs based on this analysis be welcome?

📚 Resources


⚠️ Important Notes

  • This analysis is based on static code review and profiling best practices
  • Actual performance improvements depend on:
    • Hardware characteristics (CPU, cache sizes)
    • Specific board configurations being solved
    • Compiler optimization settings (-O3, -flto, etc.)
    • Whether bottlenecks are CPU-bound vs. memory-bound
  • Profiling on representative data is essential before optimization work begins

Status: Active — #252 merged (branch prediction hints in quick_tricks.cpp, ~1% improvement confirmed on Linux). Next candidates: TransTableL::lookup_cards (4.99%) and MoveGen123 (10.65%) per post-merge perf report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions