diff --git a/README.md b/README.md index efda536..ab7fa41 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,51 @@ The features are `foundation`, `physics`, `orbit`, `rf`, `network`, `ops`, everything including `sim`; `ops` is every layer except `sim`, for a project that does not run simulations. +## Performance + +The three benchmarks are examples of the facade. They run in release mode and +depend on nothing outside the workspace, so a build is the compiler and +nothing else. The figures below are one run on an AMD Ryzen 7 8845HS (eight +cores, sixteen threads); absolute times move with the machine, so compare the +ratios within a table. The method, every size in the sweeps and the reasoning +behind each conclusion are in +[Benchmark method and results](docs/design/bench.md). + +**One call on the hot paths** + +| Path | Per call | +| --- | --- | +| Hermite ephemeris query | 24 ns | +| TEME to Earth-fixed rotation | 27 ns | +| Keplerian elements to a state | 38 ns | +| Earth-fixed to geodetic | 50 ns | +| Link budget C/N0 | 61 ns | +| SGP4 propagation, thirty minutes from epoch | 249 ns | +| Access search, 120 samples | 7.5 us | +| Planning 60 tasks on one resource | 239 us | + +An ephemeris query is an order of magnitude cheaper than the propagation it +stands in for, which is why a densely queried track is tabulated once and then +interpolated. + +**Growth with size.** A flat cost per unit is what shows the complexity is +right: + +- A day of a constellation at a sixty second step is linear in the satellite + count: 24 satellites in 8.7 ms, 2016 in 0.68 s, about 234 ns a sample, an + exponent of 0.00. +- Access search is flat at about 7.3 us a track from 8 to 128 tracks. +- Topology construction is about O(n squared) and greedy planning eases from + O(n squared) towards O(n); both are limited by their algorithm, and the + design note says why. + +**Threads.** The numerical kernels split by satellite. Propagating 2000 +satellites over a day takes 671 ms on one thread and 85 ms on sixteen, a +speedup of 7.9x; access search over 256 tracks reaches 5.3x. Efficiency is +about eighty-five percent at eight threads, one per core; sixteen puts two on +a core and returns about half of that. Every parallel result equals the serial one, +so threading buys throughput and never changes a number. + ## Conventions - Toolchain `stable`, edition 2024, rust-version 1.85 @@ -124,6 +169,7 @@ crate cannot infer: - [Errors and tolerances](docs/design/errors.md) - [Layer boundaries](docs/design/layers.md) - [Parallelism](docs/design/parallel.md) +- [Benchmark method and results](docs/design/bench.md) - [Glossary and formula conventions](docs/design/glossary.md) - [The simulation layer](docs/design/sim.md) diff --git a/README.zh.md b/README.zh.md index db211ba..07a24bf 100644 --- a/README.zh.md +++ b/README.zh.md @@ -62,6 +62,43 @@ apsis-rs = { path = "...", features = ["orbit"] } 每个层 feature 自动启用其下层。`full` 启用全部层(含 `sim`);`ops` 等价于除 `sim` 以外的所有层,适合不运行仿真的项目。 +## 性能 + +三个基准都是门面的 example。它们在 release 下运行,且除本工作区外不依赖任何 +东西,故一次构建只需要编译器本身。下表是 AMD Ryzen 7 8845HS(8 核 16 线程) +上的一次运行;绝对时间随机器变化,应比较同一张表内的比例。方法、每次规模扫描 +的全部数据以及每条结论的依据见 +[压测方法与报告](docs/design/bench.zh.md)。 + +**热点路径的单次调用** + +| 路径 | 每次调用 | +| --- | --- | +| Hermite 星历插值查询 | 24 ns | +| TEME 到地固系旋转 | 27 ns | +| 开普勒根数转状态 | 38 ns | +| 地固系转大地坐标 | 50 ns | +| 链路预算 C/N0 | 61 ns | +| SGP4 传播(历元后 30 分) | 249 ns | +| 可见性搜索(120 样本) | 7.5 μs | +| 规划 60 个任务(1 资源) | 239 μs | + +星历查询比它所代替的那次传播便宜一个数量级,这正是被密集查询的轨道应先建表 +再插值的原因。 + +**随规模的增长。** 每单位成本持平才说明复杂度正确: + +- 星座一日传播对星数线性:24 星 8.7 ms,2016 星 0.68 s,每样本约 234 ns, + 增长指数 0.00。 +- 可见性搜索在 8 到 128 轨之间恒为每轨约 7.3 μs。 +- 拓扑构建约为 O(n 平方),贪心规划由 O(n 平方) 缓向 O(n);两者都受算法而非 + 实现限制,设计说明给出了原因。 + +**并行。** 数值内核按卫星切分。2000 星一日传播单线程 671 ms、16 线程 85 ms, +加速 7.9 倍;256 轨的可见性搜索加速 5.3 倍。8 线程每核一线程时效率约八成五; +16 线程每核两线程,收益约为一半。并行结果与串行逐位相同,故加速只换来吞吐, +从不改变数值。 + ## 约定 - toolchain:`stable`,`edition 2024`,`rust-version 1.85` @@ -120,6 +157,14 @@ GPL-3.0-or-later,见 `LICENSE`。 `docs/vendor/mathjax` 下内置的 MathJax 为 Apache-2.0,见该目录内的 `LICENSE`。 -## 术语 +## 文档 +设计说明记录了单个 crate 的读者无法推断的跨 crate 决策: + +- [单位与坐标](docs/design/units.zh.md) +- [错误与容差](docs/design/errors.zh.md) +- [层边界](docs/design/layers.zh.md) +- [并行](docs/design/parallel.zh.md) +- [压测方法与报告](docs/design/bench.zh.md) - [术语与公式约定](docs/design/glossary.zh.md) +- [仿真层](docs/design/sim.zh.md) diff --git a/docs/design/bench.md b/docs/design/bench.md index ec0268d..d7bc13b 100644 --- a/docs/design/bench.md +++ b/docs/design/bench.md @@ -63,14 +63,14 @@ tool, not values across machines. | 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 | +| Hermite ephemeris query | 24.3 ns | +| TEME to Earth-fixed rotation | 26.8 ns | +| Keplerian elements to a state | 38.1 ns | +| Earth-fixed to geodetic | 50.1 ns | +| Link budget C/N0 | 60.6 ns | +| SGP4 propagation, thirty minutes from epoch | 248.7 ns | +| Access search, 120 samples | 7.49 us | +| Planning 60 tasks on one resource | 239 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: @@ -82,10 +82,10 @@ when a track is queried many times, build the table once and interpolate. | 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 | +| 24 | 8.7 ms | 8.7 ms | 252 ns | +| 240 | 80.5 ms | 80.0 ms | 231 ns | +| 1008 | 339.5 ms | 339.5 ms | 234 ns | +| **2016** | 679.4 ms | 679.4 ms | 234 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 @@ -98,20 +98,20 @@ allocation. | Tracks | Per track | | --- | --- | | 8 | 7.3 us | -| 128 | 7.4 us | +| 128 | 7.3 us | -**Flat, an exponent of 0.01.** Each track is independent, so the search is +**Flat, an exponent of 0.00.** 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 | +| 8 | 237 ns | — | +| 64 | 2.2 us | 1.07 | +| 120 | 5.7 us | 1.52 | +| 300 | 33.0 us | 1.93 | +| **600** | **182.6 us** | 2.47 | **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 @@ -122,10 +122,10 @@ judge. The parallel scan confirms where the time goes. | 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 | +| 60 | 4.5 us | — | +| 200 | 16.0 us | 1.05 | +| 1000 | 108.4 us | 1.33 | +| **2000** | **314.1 us** | 1.54 | **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 @@ -138,63 +138,65 @@ defect in the implementation. | 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 | +| 550 | 184 ms | 1.96x | 3.83x | 6.86x | 8.12x | 8.12x at 16 | +| 1000 | 336 ms | 1.89x | 3.73x | 6.49x | 8.24x | 8.24x at 16 | +| 2000 | 671 ms | 1.89x | 3.74x | 6.61x | 7.93x | 7.93x 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. +core. Eight threads is the better setting, about eighty-five percent +efficiency; sixteen buys more absolute throughput at a lower return. ### Access search | 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 | +| 64 | 0.47 ms | 2.74x | 2.68x | 1.87x | 2.74x at 4 | +| 256 | 1.95 ms | 3.52x | 4.36x | 5.27x | 5.27x at 16 | **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. +5.3x. ### 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 | +| 120 | 0.31 ms | 0.30 ms | 1.37x | +| 300 | 1.93 ms | 3.84 ms | 1.72x | -**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. +**Threading helps this path only a little.** At 300 satellites the serial +edge fold takes twice the parallelisable part, so the total gains at most +1.7x rather than the near-linear gain of the numerical kernels. 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 | +| SGP4 propagation | O(1) a sample | excellent, about 8x on sixteen | 2016 satellites a day in 0.68 s, 85 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 | +| Access search | O(1) a satellite | good, about 5x on sixteen | 256 tracks in 0.37 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 | +| Task planning | about O(n squared) | serial | 2000 tasks in 0.63 s | +| Topology construction | about O(n squared) | limited, a serial tail dominates | 600 satellites in 110 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. + thousand satellites costs 0.68 seconds on one core and 85 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. + duplicate check on edge insertion squares that quadratic; the second gains + only about 1.7x from threading. Both would need a different algorithm to + gain more. +4. Eight threads is the better setting for propagation. Efficiency is about + eighty-five percent, while sixteen threads falls to about one half because + two threads share a core. ## Reproducing diff --git a/docs/design/bench.zh.md b/docs/design/bench.zh.md index 52150ff..b061ee2 100644 --- a/docs/design/bench.zh.md +++ b/docs/design/bench.zh.md @@ -55,14 +55,14 @@ cargo run --release -p apsis-rs --features full --example bench_parallel | 路径 | 每次调用 | | --- | --- | -| Hermite 星历插值查询 | 24.5 ns | -| TEME 到地固系旋转 | 27.2 ns | -| 开普勒根数转状态 | 37.8 ns | -| 地固系转大地坐标 | 49.4 ns | -| 链路预算 C/N0 | 61.0 ns | -| SGP4 传播(历元后 30 分) | 246.7 ns | -| 可见性搜索(120 样本) | 7.46 μs | -| 规划 60 个任务(1 资源) | 240 μs | +| Hermite 星历插值查询 | 24.3 ns | +| TEME 到地固系旋转 | 26.8 ns | +| 开普勒根数转状态 | 38.1 ns | +| 地固系转大地坐标 | 50.1 ns | +| 链路预算 C/N0 | 60.6 ns | +| SGP4 传播(历元后 30 分) | 248.7 ns | +| 可见性搜索(120 样本) | 7.49 μs | +| 规划 60 个任务(1 资源) | 239 μs | 星历查询比它所替代的 SGP4 传播快约十倍,这正是 `apsis-ephemeris` 存在的理由: 一次建表、多次查询时,插值远便宜于传播。 @@ -73,10 +73,10 @@ cargo run --release -p apsis-rs --features full --example bench_parallel | 星数 | 冷启动 | 稳态 | 每样本 | | --- | --- | --- | --- | -| 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 | +| 24 | 8.7 ms | 8.7 ms | 252 ns | +| 240 | 80.5 ms | 80.0 ms | 231 ns | +| 1008 | 339.5 ms | 339.5 ms | 234 ns | +| **2016** | 679.4 ms | 679.4 ms | 234 ns | **每样本成本恒定(增长指数 0.00)**,即 SGP4 单次传播是 O(1),总时间线性于星数。 两千余星的一日覆盖约 0.7 秒。冷启动与稳态几乎相同,说明该路径没有显著的 @@ -87,19 +87,19 @@ cargo run --release -p apsis-rs --features full --example bench_parallel | 轨数 | 每星 | | --- | --- | | 8 | 7.3 μs | -| 128 | 7.4 μs | +| 128 | 7.3 μs | -**恒定(增长指数 0.01)**,每条轨道独立,线性于星数。 +**恒定(增长指数 0.00)**,每条轨道独立,线性于星数。 ### 拓扑构建与路由(4500 km 链路) | 星数 | 每星 | 增长指数 | | --- | --- | --- | -| 8 | 252 ns | — | -| 64 | 2.2 μs | 1.19 | -| 120 | 5.8 μs | 1.52 | -| 300 | 34.1 μs | 1.94 | -| **600** | **193.4 μs** | 2.50 | +| 8 | 237 ns | — | +| 64 | 2.2 μs | 1.07 | +| 120 | 5.7 μs | 1.52 | +| 300 | 33.0 μs | 1.93 | +| **600** | **182.6 μs** | 2.47 | **约 O(n²)**。每星成本随星数增长,指数从 1 升到 2.5。成因是几何判定本身 就是全对检查:n 颗星有 n² 对要判断。并行扫描证实瓶颈在此。 @@ -108,10 +108,10 @@ cargo run --release -p apsis-rs --features full --example bench_parallel | 任务数 | 每任务 | 增长指数 | | --- | --- | --- | -| 60 | 4.6 μs | — | -| 200 | 14.7 μs | 0.96 | -| 1000 | 106.7 μs | 1.32 | -| **2000** | **312.7 μs** | 1.55 | +| 60 | 4.5 μs | — | +| 200 | 16.0 μs | 1.05 | +| 1000 | 108.4 μs | 1.33 | +| **2000** | **314.1 μs** | 1.54 | **约 O(n²),大 n 时趋向 O(n)**。贪心放置本质上按时间序逐个安排,每次放置 都要与已放置的 placement 比较,故二次是算法本身而非实现缺陷。 @@ -122,53 +122,53 @@ cargo run --release -p apsis-rs --features full --example bench_parallel | 星数 | 1 线程 | 2 | 4 | 8 | 16 | 最佳加速 | | --- | --- | --- | --- | --- | --- | --- | -| 550 | 299 ms | 1.46× | 2.87× | 4.49× | 7.96× | 7.96× (16) | -| 1000 | 361 ms | 2.01× | 4.03× | 7.88× | 9.94× | 9.94× (16) | -| 2000 | 673 ms | 1.79× | 3.59× | 6.89× | 8.28× | 8.28× (16) | +| 550 | 184 ms | 1.96× | 3.83× | 6.86× | 8.12× | 8.12× (16) | +| 1000 | 336 ms | 1.89× | 3.73× | 6.49× | 8.24× | 8.24× (16) | +| 2000 | 671 ms | 1.89× | 3.74× | 6.61× | 7.93× | 7.93× (16) | **传播是 embarrassingly parallel**:每个卫星独立,2 至 8 线程接近线性。 -16 线程效率降到约 0.5,即超线程带来争用。**推荐 8 线程**(效率 0.9 左右), +16 线程效率降到约 0.5,即超线程带来争用。**推荐 8 线程**(效率约 0.85), 或 16 线程换取更高绝对吞吐。 ### 可见性搜索 | 轨数 | 1 线程 | 4 | 8 | 16 | 最佳加速 | | --- | --- | --- | --- | --- | --- | -| 64 | 1.08 ms | 2.24× | 2.89× | 2.23× | 2.89× (8) | -| 256 | 2.40 ms | 3.32× | 5.59× | 5.61× | 5.61× (8) | +| 64 | 0.47 ms | 2.74× | 2.68× | 1.87× | 2.74× (4) | +| 256 | 1.95 ms | 3.52× | 4.36× | 5.27× | 5.27× (16) | -**小规模加速有限**,因为线程启动的固定开销在小工作量上占主导。256 轨约 5.6×。 +**小规模加速有限**,因为线程启动的固定开销在小工作量上占主导。256 轨约 5.3×。 ### 拓扑构建(阶段拆分) | 星数 | 可见性(可并行) | 边折叠(串行尾部) | 最佳加速 | | --- | --- | --- | --- | -| 120 | 0.30 ms | 0.21 ms | 1.48× | -| 300 | 1.92 ms | 4.06 ms | ~1.0× | +| 120 | 0.31 ms | 0.30 ms | 1.37× | +| 300 | 1.93 ms | 3.84 ms | 1.72× | -**并行化无法解决该路径**。300 星时串行的边折叠占 4.06 ms,是可并行部分的 -两倍有余,总时间几乎不随线程数变化。根因是 `add_edge` 查重时线性扫描已有边, -使插入本身成为 O(n³)。 +**该路径并行收益有限**。300 星时串行的边折叠占 3.84 ms,是可并行部分的两倍, +故总时间最多加速约 1.7 倍,远不及数值内核的接近线性。根因是 `add_edge` 查重时 +线性扫描已有边,使插入本身成为 O(n³)。 ## 结论 | 路径 | 复杂度 | 并行性 | 实用规模表现 | | --- | --- | --- | --- | -| SGP4 传播 | O(1)/样本 | 优秀,8 线程 ~7× | 2016 星一日 0.7 s;16 线程 81 ms | +| SGP4 传播 | O(1)/样本 | 优秀,16 线程 ~8× | 2016 星一日 0.68 s;16 线程 85 ms | | 星历插值查询 | O(log n) | 随传播 | 24 ns/次 | -| 可见性搜索 | O(1)/星 | 良好,8 线程 ~5× | 256 轨 0.33 ms | +| 可见性搜索 | O(1)/星 | 良好,16 线程 ~5× | 256 轨 0.37 ms | | 坐标与链路 | O(1) | 不必并行 | 27 至 61 ns | -| 任务规划 | ~O(n²) | 串行 | 2000 任务 0.6 s | -| 拓扑构建 | ~O(n²) | **无效**(串行尾部主导) | 600 星 116 ms | +| 任务规划 | ~O(n²) | 串行 | 2000 任务 0.63 s | +| 拓扑构建 | ~O(n²) | 收益有限(串行尾部主导) | 600 星 110 ms | **要点** -1. **数值内核是线性且高度可并行的。** 两千余星的一日轨道预测单线程 0.7 秒, - 16 线程 81 毫秒。 +1. **数值内核是线性且高度可并行的。** 两千余星的一日轨道预测单线程 0.68 秒, + 16 线程 85 毫秒。 2. **星历插值比传播快十倍**,密集查询场景应先用传播建表再插值。 3. **两个组合算法受限于算法而非并行度**:任务规划是贪心的固有二次,拓扑构建的 - 边插入查重是二次的平方。并行化对二者都无能为力,需要改算法。 -4. **推荐配置 8 线程**:并行效率约 0.9,16 线程因超线程争用降至约 0.5。 + 边插入查重是二次的平方,后者并行化最多只能加速约 1.7 倍。欲再提升需改算法。 +4. **传播推荐 8 线程**:并行效率约 0.85,16 线程因超线程争用降至约 0.5。 ## 复现