122 lines
6.1 KiB
Markdown
122 lines
6.1 KiB
Markdown
# 并行化
|
||
|
||
本文件记录并行化的判定标准、提供的接口,以及实测结果。跨层的单位与坐标约定见
|
||
[单位与坐标约定](units.md),分层边界见 [分层边界](layers.md)。
|
||
|
||
## 为什么不引入线程池
|
||
|
||
工作区禁止任何第三方 crate,因此没有 `rayon` 这样的线程池可用。也不需要:
|
||
工作区中值得多核的运算,无一例外都是**逐元素独立**的循环,用标准库的
|
||
`std::thread::scope` 就能直接表达。
|
||
|
||
作用域线程按引用借用数据,调用方无需 `Arc` 即可共享一个切片或一个只读对象;
|
||
作用域结束前会 join 全部线程,因此不会有结果在线程仍在写时被读走。
|
||
|
||
## 接口
|
||
|
||
### apsis-math:并行原语
|
||
|
||
`apsis_math::parallel` 提供两个入口,是所有并行接口的基础:
|
||
|
||
| 函数 | 用途 |
|
||
| --- | --- |
|
||
| `thread_count()` | 机器的并行度 |
|
||
| `threads_for(len)` | 该长度下值得启用的线程数 |
|
||
| `for_each_index(len, f)` | 对每个下标调用 `f` |
|
||
| `map_index(len, f)` | 每个下标产出一个值,按下标序返回 |
|
||
|
||
两个入口都只要求闭包可**同步调用**(`Fn` + `Sync`),因为它们把输入当作共享
|
||
的、把输出写进各自负责的槽位;调用方不需要自己同步。
|
||
|
||
`threads_for` 在长度小于每线程一千项时返回 1:在这个量级上,启动线程的成本
|
||
高于它能省下的工作。
|
||
|
||
### 各层的批处理接口
|
||
|
||
| crate | 接口 | 适用规模 |
|
||
| --- | --- | --- |
|
||
| `apsis-sgp4` | `propagate_minutes_at_parallel` | 长轨道(≥ 数万样本) |
|
||
| `apsis-sgp4` | `propagate_at_parallel` | 整壳一次性传播 |
|
||
| `apsis-access` | `find_all_parallel` | 数百条轨道 |
|
||
| `apsis-isl` | `visibility_matrix_parallel` | 数百颗以上(见下) |
|
||
|
||
每个接口都有一个同名的顺序版本,两者结果逐元素相同、顺序相同,调用方可自由
|
||
切换。
|
||
|
||
## 判定标准
|
||
|
||
并非所有批处理都值得并行化。判定依据是**单次工作成本**与**总量**:
|
||
|
||
- **值得并行**:单次工作昂贵(≥ 微秒),且逐元素独立,规模可达数百次以上。
|
||
典型是 SGP4 传播(约 230 ns/样本,百万样本量级)与可见性搜索(约 7 μs/轨)。
|
||
- **不值得并行**:
|
||
- `Constellation::instantiate`:纯算术,几百颗星只需微秒,线程开销完全淹没。
|
||
- 调度器:贪心放置本质串行,每次放置改变后续任务的可行区间,不是独立
|
||
循环,因此没有并行接口;但其中的搜索已改为单次扫描(见下)。
|
||
- 事件引擎:单队列时序,无法分块。
|
||
- 大地与坐标变换:单点调用本身已是 O(1),没有批量。
|
||
- `mutual_visibility` 单次:约 4 ns,**单次**不值得,但成矩阵(见下)。
|
||
|
||
## 实测(16 核,release)
|
||
|
||
### SGP4 单条轨道(`propagate_minutes_at_parallel`)
|
||
|
||
| 轨道长度 | 顺序 | 并行 | 加速 |
|
||
| --- | --- | --- | --- |
|
||
| 1 天(1,440 样本) | 0.3 ms | 0.3 ms | 1.28× |
|
||
| 7 天(10,080) | 2.3 ms | 0.9 ms | 2.46× |
|
||
| 30 天(43,200) | 10.1 ms | 1.9 ms | 5.45× |
|
||
| 365 天(525,600) | 144.1 ms | 26.0 ms | 5.54× |
|
||
|
||
**结论**:轨道越长加速越高,30 天以上稳定在 5.5×。一天以内收益很小——
|
||
1,440 个样本分给多线程后每线程不足一千项,低于 `threads_for` 的阈值。
|
||
|
||
**实践要点**:有大量短轨道时,应使用 `propagate_at_parallel`(整壳一次调用),
|
||
而不是对每颗星各调一次并行版——后者会让每颗星都付一次线程启动成本。
|
||
|
||
### 可见性矩阵(`visibility_matrix_parallel`)
|
||
|
||
| 星数 | 对数 | 顺序 | 并行 | 加速 |
|
||
| --- | --- | --- | --- | --- |
|
||
| 200 | 4 万 | 0.16 ms | 0.31 ms | 0.53× |
|
||
| 600 | 36 万 | 1.50 ms | 1.37 ms | 1.09× |
|
||
| 1,000 | 100 万 | 4.09 ms | 2.50 ms | 1.64× |
|
||
|
||
**结论**:单次可见性判定只有几纳秒,**小规模下并行反而更慢**(线程开销占比高),
|
||
数百颗星以上才开始获益,且远未饱和。这个接口是为大规模壳准备的;小壳应直接用
|
||
顺序版。接口文档也如实说明了这一点。
|
||
|
||
### 跨层组合
|
||
|
||
`bench_parallel` 的测量显示,整壳在多核上传播与构建拓扑的并行效果最好,因为
|
||
它把整个工作量一次性摊给所有线程。相关数据见该示例的输出。
|
||
|
||
## 确定性
|
||
|
||
并行不改变结果:所有批量接口都逐元素比对顺序版的结果(SGP4 的状态、可见性
|
||
的布尔矩阵、access 的过站摘要),顺序也一致。传播本身是确定性的,只依赖时刻,
|
||
与如何分块无关;可见性与过站搜索是逐元素独立的。因此同一输入在任意线程数下
|
||
产生相同输出,测试也按此断言。
|
||
|
||
## 调度器搜索的优化
|
||
|
||
调度器没有并行接口,但它的最坏情况曾达到每任务数百微秒。优化分两步,两者都
|
||
保持结果逐位不变。
|
||
|
||
**第一步:一次扫描代替逐候选测试。** 旧的做法列出所有"可行性可能变化"的时刻,
|
||
逐个测试,每次都重新计算每个资源的负载曲线。负载曲线与候选时刻无关,因此改为
|
||
扫一次:把"资源没有余量的区间"从可用窗口中减去,多资源求交,取第一个存活区间
|
||
的起点。结果仍由 `feasible_at` 确认,枚举法保留为浮点边界不一致时的回退。
|
||
|
||
**第二步:确认调用复用索引。** 去掉确认调用后耗时降到八分之一,这定位了真正
|
||
的开销:确认用的公开可行性测试在收集边界时扫描一次任务表,又在每个边界上对每
|
||
个覆盖该边界的放置再扫描一次任务表。把第一步建好的索引传进去,两处扫描都变成
|
||
按标识符直接读取。
|
||
|
||
任务标识符是小而稠密的整数,因此索引用向量而非哈希表:数组下标比哈希一个整数
|
||
更便宜,这在本仓库已测到两次(拓扑的邻接表用索引、这里的任务表用向量)。
|
||
|
||
**结果**:全部任务争用单一资源、容量为瓶颈时,3200 个任务从 6.7 秒降到 0.48
|
||
秒(14 倍),6400 个任务 2.2 秒。计划结果不变——测试在两种资源、窗口不相交的
|
||
场景下逐个比对了每个放置的起止时刻。
|