aboutsummaryrefslogtreecommitdiff

Benchmarking and profiling

The bench command

cargo run --profile profiling -- bench [depth]     # default depth 6

src/bench.rs searches six fixed positions, one from each phase of the game, to a fixed depth without the TUI, and prints the move, score and time for each, plus a total:

Label Position after
opening 0 plies
tiger reply 1 ply
mid-placement 20 plies
end of placement 38 plies
early movement 45 plies
movement 60 plies

The positions come from ClaudePlayer playing itself to depth 4, which is deterministic and plays much more like a real game than random moves. Each search uses a fresh ClaudePlayer with max_depth set to the requested depth and an effectively infinite time limit, so every run does exactly the same work and timings can be compared. Depth 9 takes well under a second in total.

The rule for optimisations: a change that only makes the search faster must leave every move and every score unchanged. If they change, the change altered what the search computes, not just how fast.

Search counters

cargo run --profile profiling --features search-stats -- bench 9

With the search-stats feature, each position also prints:

  • nodes searched and nodes per second, leaves, terminal positions, repetitions, move generations per node;
  • cache probes, hit rate, the share of hits that settled the score without searching, and overwrites;
  • cutoffs, and the share of them on the first move tried, which measures move ordering.

The counters are thread-local SearchStats fields bumped by the count! macro (defined in player.rs, also used in board.rs). Without the feature count! expands to nothing. Counting slows the search a little, so compare times only between builds with the same features.

Profiling

The profiling profile is release with debug info. The dev shell includes perf and samply.

cargo build --profile profiling
perf record -e cycles:u -m 16 --call-graph dwarf target/profiling/tigers-and-sheep-rs bench 9
perf report

As a non-root user perf record needs -m 16 (a small ring buffer) with --call-graph dwarf, otherwise mmap fails against the perf_event_mlock_kb limit.

Or, for a flame graph in the browser:

samply record target/profiling/tigers-and-sheep-rs bench 9

Check the clock source first

cat /sys/devices/system/clocksource/clocksource0/current_clocksource

The search reads the clock (Instant::now()) once per interior node to check its deadline. With the tsc clock source that's a cheap instruction. If the kernel has fallen back to hpet, as has happened on the development machine, every read becomes a system call and the search runs at nearly half speed. Only compare timings taken on the same clock source.

History

plans.org records each optimisation with its before and after timings, node counts and cutoff rates, and the next candidates. Add to it when you make a change that affects speed.