summaryrefslogtreecommitdiff
path: root/docs/benchmarking.md
blob: 9d85ddf9da3cae045f3c9ddf75421fd670a9bbb3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# Benchmarking and profiling

## The `bench` command

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

[`src/bench.rs`](../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

```bash
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`.

```bash
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:

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

## Check the clock source first

```bash
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`](../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.