apsis-rs/docs/design/parallel.md

6.1 KiB
Raw Blame History

并行化

本文件记录并行化的判定标准、提供的接口,以及实测结果。跨层的单位与坐标约定见 单位与坐标约定,分层边界见 分层边界。

为什么不引入线程池

工作区禁止任何第三方 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 秒。计划结果不变——测试在两种资源、窗口不相交的 场景下逐个比对了每个放置的起止时刻。