Python 36:一次快了能说明优化有效吗
四次词频任务,某次顺序运行只用了约 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 | |
环境、全部暖机和 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 口径和环境,再讨论适用范围。
系列导航只列已验收文章。




