d930fb558c
对比 WorkBuddy/Claude/Codex 三款 AI 工具对同一代码库的 优化建议,以表格形式评价各自优劣(Bug 发现/代码质量/战略 思维/代码示例),最终整合为 6 阶段实施计划(11 人天)。
352 lines
11 KiB
Markdown
352 lines
11 KiB
Markdown
# Dynamics 项目优化建议
|
||
|
||
本文基于对以下文件的静态检查整理:
|
||
|
||
- `compute.py`
|
||
- `dynamics.py`
|
||
- `draw.py`
|
||
- `engines/c/main.c`
|
||
- `README.md`
|
||
|
||
目标不是泛泛地“提速”,而是优先找出这个项目当前最可能影响性能、内存占用、可维护性和后续扩展效率的点,并给出按优先级排序的改进方向。
|
||
|
||
## 一、整体判断
|
||
|
||
这个项目现在的架构已经有一个很好的基础:`dynamics.py` 做流程编排,`compute.py` 做 Python 参考实现,`engines/c/main.c` 提供高性能引擎,`draw.py` 负责可视化。
|
||
|
||
从代码结构看,当前主要瓶颈不在“外层调度”,而集中在三类地方:
|
||
|
||
1. `compute.py` 中逐步积分时的 Python 层循环。
|
||
2. `compute.py` 中弹簧力/粒子间引力的双层或逐键循环。
|
||
3. `display.txt` / `trajectory.txt` 的文本格式读写。
|
||
|
||
如果后续案例规模继续增大,Python 参考引擎会很快被这三处放大;而且即便切到 C 引擎,文本 I/O 仍然会成为新的主要瓶颈。
|
||
|
||
## 二、最高优先级建议
|
||
|
||
## 1. 先把“计算核心”和“输出格式”分开优化
|
||
|
||
当前项目已经有多语言引擎,这是很正确的方向。建议把优化目标拆成两条线分别推进:
|
||
|
||
- 计算性能:优先优化 `compute.py` 中的力计算和积分主循环。
|
||
- I/O 性能:优先替换或补充 `display.txt` / `trajectory.txt` 的文本格式。
|
||
|
||
原因:
|
||
|
||
- 计算核心的瓶颈主要是 CPU。
|
||
- 轨迹输出的瓶颈主要是字符串格式化、磁盘写入、加载解析。
|
||
- 这两类优化手段完全不同,混在一起做容易看不出收益来源。
|
||
|
||
建议先增加一个简单的 profiling 开关,例如输出:
|
||
|
||
- 总模拟时间
|
||
- 力计算时间
|
||
- 抽帧时间
|
||
- 保存 `display.txt` 时间
|
||
- 保存 `trajectory.txt` 时间
|
||
- 加载 `display.txt` 时间
|
||
|
||
这样后面每做一次优化,都能知道收益来自哪一段。
|
||
|
||
## 2. 优先向量化 `compute_force()` 里的弹簧键计算
|
||
|
||
`compute.py:1230` 开始的 `compute_force()` 是 Python 引擎最关键的热点。
|
||
|
||
其中弹簧键部分目前是:
|
||
|
||
- 遍历 `BOND_PAIRS`
|
||
- 每根键单独算 `dx/dy/dz`
|
||
- 每根键单独回写两个原子的受力
|
||
|
||
这在键数增大后会变成明显瓶颈。
|
||
|
||
建议做法:
|
||
|
||
- 把 `BOND_PAIRS[:, 0]` 和 `BOND_PAIRS[:, 1]` 拆成两个索引数组 `i_idx`、`j_idx`。
|
||
- 用 NumPy 一次性计算所有键的 `dx/dy/dz/dist/stretch`。
|
||
- 用 `np.add.at()` 或等价的 scatter 累加方式,把键力一次性回写到 `fx/fy/fz`。
|
||
|
||
预期收益:
|
||
|
||
- 中等规模体系下,Python 参考引擎会有很可观的提速。
|
||
- 这也是最不改变项目结构、最容易验证正确性的优化。
|
||
|
||
## 3. 把原子间万有引力视为“可选高成本模块”,不要默认走 Python 双层循环
|
||
|
||
`compute.py:1281` 开始的 `GRAVITY_INTERACTION` 采用 `for i` / `for j` 双层循环,复杂度是 `O(N^2)`。
|
||
|
||
这段代码在教学上没问题,但在稍大的粒子数下会非常慢。建议:
|
||
|
||
- 明确把它标记为“仅适合小规模案例”。
|
||
- 默认推荐用户用 `engine: c` 跑这类 case。
|
||
- 如果未来仍想保留 Python 版本,优先考虑:
|
||
- 小规模时保持现状。
|
||
- 中大规模时改成 Numba 或 C 扩展。
|
||
- 更进一步再考虑 Barnes-Hut、cell list、neighbor list 之类近似/加速算法。
|
||
|
||
如果只是当前项目阶段,我更建议:
|
||
|
||
- 不要先在 Python 里硬做复杂天体优化。
|
||
- 先把这类高复杂度场景明确导向 C 引擎。
|
||
|
||
这是投入产出比更高的路线。
|
||
|
||
## 4. `display.txt` 文本格式要补一个二进制版本
|
||
|
||
`compute.py:196` 的 `save_display_txt()` 和 `compute.py:239` 的 `load_display_txt()` 已经比 JSON 好很多,但本质仍然是大文本。
|
||
|
||
当前问题:
|
||
|
||
- 保存时每个数都在做字符串格式化。
|
||
- 读取时虽然用了 `np.genfromtxt`,但源数据仍是文本。
|
||
- 帧数和粒子数继续增大后,I/O 会成为明显瓶颈。
|
||
|
||
建议保留 `display.txt` 作为“人可读格式”,同时新增一个高性能格式,例如:
|
||
|
||
- `display.npz`
|
||
- 或 `display.npy` + `meta.json`
|
||
|
||
推荐方案:
|
||
|
||
- `frames_x/y/z/vx/vy/vz` 存到 `np.savez_compressed`
|
||
- 元信息单独存一个轻量 `meta.json`
|
||
- `draw.py` 优先读二进制,不存在时再回退到 `display.txt`
|
||
|
||
这样有几个好处:
|
||
|
||
- 调试和教学仍可保留文本文件。
|
||
- 大规模运行时可以直接避开文本解析开销。
|
||
- Python 和 C 引擎都可以逐步迁移,不需要一次切完。
|
||
|
||
## 三、中优先级建议
|
||
|
||
## 5. `run_simulation()` 里完整轨迹缓存要改成“按需流式写出”
|
||
|
||
`compute.py:1481` 起,如果 `save_trajectory=1`,会一次性申请:
|
||
|
||
- `traj_x`
|
||
- `traj_y`
|
||
- `traj_z`
|
||
- `traj_vx`
|
||
- `traj_vy`
|
||
- `traj_vz`
|
||
|
||
这意味着内存复杂度接近 `O(steps * atoms)`,而且还是 6 份 `float64` 数组。
|
||
|
||
这在教学小案例里没问题,但一旦:
|
||
|
||
- `NT` 很大
|
||
- 粒子数上来
|
||
- 同时还保留抽帧缓存
|
||
|
||
内存会膨胀得很快。
|
||
|
||
建议:
|
||
|
||
- 如果只是为了最终导出,改成边算边写。
|
||
- 如果还需要后续随机访问,可以改成 `memmap` 或分块写入 `npz/hdf5/zarr`。
|
||
|
||
推荐优先级:
|
||
|
||
- 先做“保存完整轨迹时改为 chunked binary writer”。
|
||
- `trajectory.txt` 保留为兼容输出,而不是默认主输出。
|
||
|
||
## 6. `draw.py` 的 Marker 更新应一次性切片赋值,避免逐原子 Python 循环
|
||
|
||
`draw.py:517` 附近的 `_update_atom_positions()` 在 `USE_MARKER` 模式下仍然逐原子循环:
|
||
|
||
- 先 `for i in range(N_ATOMS)`
|
||
- 再逐个写 `marker_pos[i]`
|
||
- 然后 `balls.set_data(pos=marker_pos)`
|
||
|
||
建议直接改成:
|
||
|
||
- `marker_pos[:, 0] = DISP_ALL_X[f_idx]`
|
||
- `marker_pos[:, 1] = DISP_ALL_Y[f_idx]`
|
||
- `marker_pos[:, 2] = DISP_ALL_Z[f_idx]`
|
||
|
||
这样更符合 Marker 模式“批量更新”的初衷。
|
||
|
||
同理,成键线 `_update_bond_positions()` 也可以进一步尝试批量索引构造,而不是逐键更新。
|
||
|
||
虽然这部分通常不如计算核心慢,但在大粒子数动画里会直接影响帧率。
|
||
|
||
## 7. `apply_fixed_constraints()` 每步构造 `column_stack`,可以改成原地掩码写回
|
||
|
||
`compute.py:1408` 的实现每步都会:
|
||
|
||
- `np.column_stack((x, y, z))`
|
||
- `np.column_stack((vx, vy, vz))`
|
||
- `np.where(...)`
|
||
|
||
这会产生额外临时数组。
|
||
|
||
建议改成:
|
||
|
||
- 预先缓存 `fixed_x/fixed_y/fixed_z` 三个布尔掩码
|
||
- 直接对 `x/y/z/vx/vy/vz` 原地赋值
|
||
|
||
例如思路上改成:
|
||
|
||
- `x[fixed_x] = ATOM_POSITIONS[fixed_x, 0]`
|
||
- `vx[fixed_x] = 0.0`
|
||
|
||
这类优化单项收益不一定最大,但由于它在每一步都执行,累计下来是有价值的。
|
||
|
||
## 8. `apply_driving_force()` 有重复小数组分配
|
||
|
||
`compute.py:599` 的驱动力逻辑中,每个 driver、每一步都构造:
|
||
|
||
- `t_vec = np.array([t, t, t], dtype=np.float64)`
|
||
|
||
这是典型的小对象重复分配。
|
||
|
||
建议:
|
||
|
||
- 直接分别计算三个轴,不需要生成 `t_vec`
|
||
- 或预先把 driver 参数整理成矩阵,批量更新受驱原子
|
||
|
||
如果 driver 数量很少,这不是最大瓶颈;但这类微优化很容易做,而且不会增加复杂度。
|
||
|
||
## 四、架构与可维护性建议
|
||
|
||
## 9. 减少 `compute.py` 的全局变量依赖,逐步收敛到状态对象
|
||
|
||
目前 `compute.py` 依赖大量全局变量,例如:
|
||
|
||
- `ATOM_IDS`
|
||
- `ATOM_MASSES`
|
||
- `BOND_PAIRS`
|
||
- `METHOD`
|
||
- `NT`
|
||
- `DT`
|
||
|
||
这让代码在以下场景下会越来越难维护:
|
||
|
||
- 并行运行多个 case
|
||
- 写单元测试
|
||
- 替换不同 force model
|
||
- 将来做 GUI 或服务化封装
|
||
|
||
建议中期做一个 `SimulationState` / `SimulationConfig` / `SystemData` 分层:
|
||
|
||
- 配置类:步长、方法、开关、输出选项
|
||
- 系统类:原子、键、边界、驱动参数
|
||
- 状态类:当前 `x/y/z/vx/vy/vz`
|
||
|
||
不需要一次性重构完,先从最核心的 `compute_force()`、`run_simulation()` 入手即可。
|
||
|
||
## 10. Python 参考引擎和 C 引擎的“功能等价层”建议更明确
|
||
|
||
README 里已经强调了多语言对比,这是项目亮点。为了后续更稳,建议把“等价层”标准化:
|
||
|
||
- 相同输入
|
||
- 相同输出语义
|
||
- 相同采样规则
|
||
- 相同边界行为
|
||
- 相同 driver 行为
|
||
|
||
然后做一个最小一致性测试集:
|
||
|
||
- case01-case06 都能跑
|
||
- Python 与 C 输出在容差内一致
|
||
- 不同 method 的结果有基准对照
|
||
|
||
这样后续做任何优化时,都更容易大胆改,不怕悄悄改坏物理行为。
|
||
|
||
## 五、需要尽快处理的正确性/维护风险
|
||
|
||
这些不一定直接是“性能问题”,但会影响后续优化效率,建议优先修一下。
|
||
|
||
## 11. `dynamics.py` 的绘图分支存在明显变量依赖不完整的风险
|
||
|
||
在 `dynamics.py:331` 一带,绘图代码直接使用:
|
||
|
||
- `all_x`
|
||
- `all_y`
|
||
- `all_z`
|
||
- `all_vx`
|
||
- `all_vy`
|
||
- `all_vz`
|
||
- `data`
|
||
|
||
但从当前文件上下文看,这些变量只在某些分支里才会存在,尤其 Python 引擎路径下很可能未定义。
|
||
|
||
这会带来两个问题:
|
||
|
||
- 某些 case 可能直接在绘图阶段报错。
|
||
- 优化时很难判断问题来自性能还是流程分支。
|
||
|
||
建议:
|
||
|
||
- 在进入绘图逻辑前统一构造标准数据对象。
|
||
- 不要依赖分支里“顺便留下来的局部变量”。
|
||
|
||
## 12. `README.md` 描述与当前实现已经有部分不一致
|
||
|
||
例如 README 仍强调:
|
||
|
||
- `trajectory.txt (JSON, 统一格式)`
|
||
- `sample.py -> display.txt`
|
||
|
||
但当前实现里:
|
||
|
||
- Python 路径已经直接写 `display.txt`
|
||
- `save_trajectory` 变成可选
|
||
- `sample.py` 在主流程中的角色已经下降
|
||
|
||
文档不一致本身不会拖慢程序,但会拖慢后续协作和排障效率,尤其在你继续演进输出格式时会更明显。
|
||
|
||
建议在做 I/O 优化时顺手更新 README,避免认知分叉。
|
||
|
||
## 六、推荐实施顺序
|
||
|
||
如果按“最少改动获得最大收益”的原则,我建议这样排:
|
||
|
||
1. 给 `dynamics.py` / `compute.py` 增加基础计时统计。
|
||
2. 向量化 `compute_force()` 的弹簧键计算。
|
||
3. 把 `draw.py` 的 Marker 更新改成切片赋值。
|
||
4. 新增 `display.npz`,`draw.py` 优先读取二进制。
|
||
5. 把 `save_trajectory=1` 改成分块二进制输出,而不是全量内存缓存。
|
||
6. 修复 `dynamics.py` 绘图分支的数据来源问题。
|
||
7. 再考虑 `compute.py` 全局变量收敛和更深层的结构重构。
|
||
|
||
## 七、如果只做三件事,最值得做什么
|
||
|
||
如果你现在只想投入一小轮精力,我建议只做这三项:
|
||
|
||
1. 向量化 `compute_force()` 的弹簧键部分。
|
||
2. 增加 `display.npz` 二进制输出与读取。
|
||
3. 修正 `dynamics.py` 绘图阶段的数据流一致性。
|
||
|
||
原因:
|
||
|
||
- 第 1 项直接优化 Python 引擎核心热点。
|
||
- 第 2 项直接优化所有引擎共享的 I/O 瓶颈。
|
||
- 第 3 项能降低后续改动时的维护风险。
|
||
|
||
这三项一起做,收益通常比零散微优化更明显。
|
||
|
||
## 八、结论
|
||
|
||
这个项目最值得肯定的地方,是已经天然分成了:
|
||
|
||
- 参考实现
|
||
- 高性能引擎
|
||
- 统一可视化管线
|
||
|
||
这意味着它非常适合做“分层优化”,不需要推倒重来。
|
||
|
||
从当前代码看,后续最有效的路线不是继续在外围加流程判断,而是:
|
||
|
||
- 把 Python 热点循环尽量向量化
|
||
- 把文本轨迹格式逐步替换为二进制主格式
|
||
- 把数据流和状态管理收紧
|
||
|
||
如果按这个方向推进,这个项目会同时得到:
|
||
|
||
- 更好的性能
|
||
- 更低的内存占用
|
||
- 更稳定的多引擎一致性
|
||
- 更容易继续扩展新的物理项和可视化形式
|