apsis-rs/docs/design/bench.md

7.8 KiB

Benchmark method and results

English | 简体中文

This file records how the benchmarks are run and what they measured, so that someone else can reproduce them and judge whether the conclusions hold.

Method

Three measurements, each answering a different question

Tool Question it answers
bench How expensive is one call
bench_scale How does the cost grow with size
bench_parallel How much do the other cores save

All three are examples of the apsis-rs facade and run in release mode:

cargo run --release -p apsis-rs --features full --example bench
cargo run --release -p apsis-rs --features full --example bench_scale
cargo run --release -p apsis-rs --features full --example bench_parallel

Three rules

No warm-up. No call is discarded before a measurement begins. The first call pays for whatever the path allocates or caches, and that is a cost the caller waits for; hiding it behind a discarded run would report a figure nobody ever waited for. bench_scale therefore reports a cold column beside the steady one.

Take the best of several runs. The serial baseline is run a few times and the fastest kept. A scheduling hiccup slows one run, not all of them, so the fastest is the clean signal rather than a floor polluted by whatever else the machine was doing. bench_parallel works this way.

Report cost per unit. The absolute figures move with the machine, the nanoseconds per unit do not. The second is what shows whether the complexity is right.

How complexity is decided

For an input size n, a constant ns per unit means constant work per item and linear total time. A ns per unit that grows as a power of n means the total is that power. The exponent is the logarithm of the ratio of two neighbouring points.

Machine

Item Value
CPU AMD Ryzen 7 8845HS, eight cores, sixteen threads
Memory 28 GB
Toolchain rustc 1.98.1, release
Dependencies none; the benchmarks are examples

The absolute numbers mean nothing off this machine. Compare ratios within one tool, not values across machines.

Result one: a single call

Path Per call
Hermite ephemeris query 24.5 ns
TEME to Earth-fixed rotation 27.2 ns
Keplerian elements to a state 37.8 ns
Earth-fixed to geodetic 49.4 ns
Link budget C/N0 61.0 ns
SGP4 propagation, thirty minutes from epoch 246.7 ns
Access search, 120 samples 7.46 us
Planning 60 tasks on one resource 240 us

A query against the ephemeris is about ten times cheaper than the propagation it stands in for, which is the reason apsis-ephemeris exists: when a track is queried many times, build the table once and interpolate.

Result two: growth with size

One day of a constellation, sixty second step

Satellites Cold Steady Per sample
24 9.1 ms 8.2 ms 238 ns
240 79.5 ms 82.5 ms 239 ns
1008 337.0 ms 337.0 ms 232 ns
2016 675.2 ms 675.2 ms 232 ns

The cost per sample is flat, an exponent of 0.00, so one SGP4 propagation is constant time and the whole day is linear in the number of satellites. A day over two thousand satellites takes about seven tenths of a second. Cold and steady are nearly equal, so the path has no significant one-off allocation.

Access search, one station

Tracks Per track
8 7.3 us
128 7.4 us

Flat, an exponent of 0.01. Each track is independent, so the search is linear in the number of satellites.

Topology construction and routing

Satellites Per satellite Exponent
8 252 ns —
64 2.2 us 1.19
120 5.8 us 1.52
300 34.1 us 1.94
600 193.4 us 2.50

Roughly O(n squared). The cost per satellite grows with the count, the exponent rising from one towards two and a half. The cause is inherent: the geometry test is an all pairs check, and n satellites have n squared pairs to judge. The parallel scan confirms where the time goes.

Task planning

Tasks Per task Exponent
60 4.6 us —
200 14.7 us 0.96
1000 106.7 us 1.32
2000 312.7 us 1.55

Roughly O(n squared), easing towards O(n) at size. A greedy placement arranges tasks in time order and each arrangement is compared against the placements already made, so the quadratic is the algorithm rather than a defect in the implementation.

Result three: threads

Propagation, split by satellite

Satellites 1 thread 2 4 8 16 Best
550 299 ms 1.46x 2.87x 4.49x 7.96x 7.96x at 16
1000 361 ms 2.01x 4.03x 7.88x 9.94x 9.94x at 16
2000 673 ms 1.79x 3.59x 6.89x 8.28x 8.28x at 16

Propagation is embarrassingly parallel. Every satellite is independent, and two to eight threads come close to linear. Efficiency at sixteen threads falls to about one half, which is the contention of running two threads per core. Eight threads is the better setting, near ninety percent efficiency; sixteen buys more absolute throughput at a lower return.

Tracks 1 thread 4 8 16 Best
64 1.08 ms 2.24x 2.89x 2.23x 2.89x at 8
256 2.40 ms 3.32x 5.59x 5.61x 5.61x at 8

The gain is small on a small batch, because the fixed cost of starting threads dominates when there is little work. At 256 tracks it reaches about 5.6x.

Topology construction, split by phase

Satellites Visibility, parallelisable Edge fold, serial tail Best speedup
120 0.30 ms 0.21 ms 1.48x
300 1.92 ms 4.06 ms about 1.0x

Threading does not help this path. At 300 satellites the serial edge fold takes more than twice the parallelisable part, and the total barely moves with the thread count. The cause is that add_edge scans the edges already stored to reject a duplicate, which makes the insertion itself cubic.

Conclusions

Path Complexity Threads At useful scale
SGP4 propagation O(1) a sample excellent, about 7x on eight 2016 satellites a day in 0.7 s, 81 ms on sixteen
Ephemeris query O(log n) follows propagation 24 ns
Access search O(1) a satellite good, about 5x on eight 256 tracks in 0.33 ms
Coordinate and link O(1) not worth it 27 to 61 ns
Task planning about O(n squared) serial 2000 tasks in 0.6 s
Topology construction about O(n squared) no gain, a serial tail dominates 600 satellites in 116 ms

The points that matter

  1. The numerical kernels are linear and parallelise well. A day over two thousand satellites costs 0.7 seconds on one core and 81 ms on sixteen.
  2. Interpolating the ephemeris is about ten times cheaper than propagating, so a densely queried track should be tabulated once.
  3. Two combined operations are limited by their algorithm rather than by the available cores. Greedy planning is quadratically serial, and the duplicate check on edge insertion squares that quadratic. Neither gains from threading; both need a different algorithm.
  4. Eight threads is the setting to use. Efficiency is near ninety percent, while sixteen threads falls to about one half because two threads share a core.

Reproducing

# The gates first, so that what is measured is the right code.
cargo test --workspace --all-features

# The three measurements.
cargo run --release -p apsis-rs --features full --example bench
cargo run --release -p apsis-rs --features full --example bench_scale
cargo run --release -p apsis-rs --features full --example bench_parallel

Do not run other work while measuring. The absolute values move with the machine, so the conclusions rest on the ratios within one tool.