四次词频任务,某次顺序运行只用了约 0.13 秒,某次却用了 0.18 秒。单次相差已足以淹没很多所谓“小优化”。在比较 29 的线程、30 的进程和 31 的 asyncio前,必须先定义测量包含哪些步骤。

benchmarks/36_measure.py 为四种策略都提交四个 word_count("One two one"),每项执行 cpu_work(..., iterations=20000),并断言结果是 {"one":2,"two":1}。每种策略先跑一次不计入统计的暖机,再取五次原始样本;计时覆盖新建 store、提交任务、执行器或进程池启动、计算、等待和关闭。输出每次 perf_counter_ns 的 wall time、process_time_ns 的父进程 CPU time、每秒完成任务数、完成和失败数。进程模式的子进程 CPU 不在父进程 process_time_ns 里。

先看测到了什么

这次在 /usr/bin/python3.12 的 CPython 3.12.3、Linux x86_64 上,各组五个 wall 样本的最小值/中位数/最大值分别为(单位毫秒,原始纳秒值未四舍五入,见证据):

策略 最小 中位 最大 失败
顺序 132.0 138.1 182.1 0
线程 173.1 178.6 195.6 0
进程 601.2 818.7 960.7 0
asyncio 160.7 185.7 325.6 0

这不是进程一定慢、asyncio 一定比线程慢的证据。当前 ProcessTaskRunner.run 每次都阻塞等一个子任务结果并在父进程额外核对词频,没有同时派发四个子任务;此表比较的是这些现有策略的端到端路径,包括 spawn 启动成本,而不是四种机制的纯计算吞吐对照。线程池同时接收四个任务,asyncio 的纯 Python CPU 循环没有等待点;改变工作规模、进程复用或派发形状,排序可能改变。计时期间还有其他系统负载,五个样本不足以声称统计显著性。

独立的 cProfile 只剖析一次顺序执行,按累计时间定位 cpu_work;剖析记录显示 880313 次函数调用、约 0.443 秒,不能拿它与无插桩 wall 中位 0.138 秒直接做性能比。剖析器回答“在哪些函数花时间”,wall time 回答“这段边界内过去了多久”,父进程 CPU time 则没有计入进程工作者的 CPU。三种数字必须连同各自范围报告。

1
2
3
cd examples/python-language
PYTHONPATH=src /usr/bin/python3.12 benchmarks/36_measure.py --samples 5 --iterations 20000
PYTHONPATH=src /usr/bin/python3.12 -m unittest discover -s tests -v

环境、全部暖机和 20 个原始样本、统计摘要、剖析输出及累计 94 项测试保存在 examples/python-language/evidence/36/20261008-measure/。不从这些数推断 free-threaded 构建或其他平台的结果,也不把进程的父 CPU 计时冒充全进程 CPU 成本。

预测题

若本次进程 wall time 较大,但父进程的 process_time_ns 小于顺序执行,能否判断进程消耗了更少的 CPU?

答案:不能。父进程计时不含子进程 CPU,进程池启动、传输和等待又被 wall 计入。要回答全进程 CPU 成本,需要另外采集子进程资源计量,并注明不同平台口径。

变更题

把工作量从 iterations=20000 改为 2000000,只保留一次执行,并从 cProfile 的总耗时断定“多进程加速十倍”,哪里缺证据?

答案:负载和测量条件都已改变,单次值没有分布;现有进程适配器还串行等待、重复核对,没有并行派发的可比合同。剖析时另有插桩开销。应在新固定输入与派发形状下重新暖机、采多个原始样本,核对结果与异常数,分别记录 wall、完整 CPU 口径和环境,再讨论适用范围。

系列导航只列已验收文章。

参考资料