原文:https://dev.to/bijay-odyssey/i-deleted-my-own-optimization-because-the-benchmark-said-it-was-21x-slower-538k(作者 @bijay-odyssey)
这次优化花了一天时间写一个 NumPy 内核,用来替换部分 pandas 代码。这是那种看起来正确的优化:按块操作,而不是按列操作;在一次遍历二维数组时计算统计矩,而不是循环 Series;完全绕开 pandas 机制。
随后把它和被替换的 pandas 代码放进同一个基准测试,然后删掉了它。
这个实现
这段代码来自 edaprep 库,它会先对 DataFrame 做画像(profiling),再决定如何预处理。画像指对每个数值列计算统计量——均值、标准差、偏度、峰度、分位数。
直观的 pandas 版本会逐列完成。直观的优化是停止逐列处理:把所有数值列作为一个连续的二维数组,沿某个轴在一次向量化遍历中计算统计矩,然后返回结果。只读一次内存,而不是 N 次。
这个推理本身没错,开发时写的微基准测试也显示它更快。这个实现当时看起来也足够令人满意。
测量
正式基准测试、真实 DataFrame 形状、候选实现交替执行、至少运行五次:
| 实现 | 耗时 | 峰值内存 |
|---|---|---|
| 手写 NumPy 块级内核 | 799 ms | 298 MiB |
| 被替换的 pandas 代码 | 557 ms | 1.6 MiB |
结果更慢,而且内存占用接近两百倍。
内存数字就是破绽,也解释了耗时。要得到“作为一个连续二维数组的数值列”,必须先物化这个数组。pandas 以块(block)形式存储 DataFrame;只要这些列不在同一个同构块里——dtype 不同、存在可空列,或任何类似情况——.to_numpy() 都会把整个数值部分复制到一块新分配的内存里。在 10 万行的 DataFrame 上,这意味着在执行任何一次算术运算前,就要先复制数百 MB 数据。
pandas 从不承担这笔开销。它直接基于已有块计算每列的统计矩,就地完成,几乎不分配内存。
这里优化的是本来就不是瓶颈的算术计算,方式却是增加一次真正的瓶颈:复制。
更扎心的部分
还有第二个版本。代码里保留了一个分支,根据列数宽度在逐列和整帧级别之间选择,理论是宽 DataFrame 可以摊薄复制成本。但分支判断写反了。在窄 DataFrame 上,它选择了整帧级别,耗时 1684 ms、峰值内存 378 MiB;而逐列路径是 543 ms 和 3.1 MiB。
所以修复方式不是修正分支,而是意识到这个分支没有任何能赢的一侧,然后删掉它。现在 profiling/statistics.py 直接委托给 pandas,这也是三个版本中最快、内存最小、代码最短的一个。
为什么写进文档,而不是只留在 git 历史里
诱惑当然是悄悄删掉这个分支,让 README 只留下好看的数据。但这里把它放进了 docs/performance.md 第 1 节,因为只写胜绩的性能文档不是证据,而是营销。产生那些优势数据的同一套基准测试工具,也产生了这个结果;愿意公布对自己不利的结果,正是其他结果可信的原因:
| 操作 | edaprep | 基线 |
|---|---|---|
| Scaler(standard) | 6.9 ms | sklearn StandardScaler 26.7 ms |
| MissingValueHandler(median) | 5.2 ms | sklearn SimpleImputer 17.0 ms |
| OutlierHandler(IQR clip) | 25.2 ms | 常规 IQR 处理块 41.4 ms |
这个库的贡献指南现在也把同样内容写成了规则:性能声明必须有基准测试支撑,并且要敢于删除不成立的优化。
一天前应该知道的三件事
- 把替代方案和被替代对象放在一起做基准测试,并且使用真实数据形状,而不是在玩具 DataFrame 上和一个稻草人循环比较。微基准测试使用的是单一 dtype 的 DataFrame,而这恰好是
.to_numpy()没有复制开销的唯一情况。 - 同时测量时间和内存。 时间结果还有足够模糊空间,可以被解释过去;298 MiB 对 1.6 MiB 则没有争辩余地,而且解释了耗时。
- “向量化”不是“快”的同义词。 它更接近“算术计算不是瓶颈”的同义词。如果为了向量化必须先复制数据,那只是把瓶颈挪了位置,而不是消除瓶颈。
这个库是 edaprep——MIT 许可、纯 Python,可通过 pip install edaprep 安装。它负责 EDA 和防泄漏预处理,并会为每个决策打印原因和对应测量值;这与公开这个基准测试是同一种本能。
它至今没有编译代码。唯一一个手写内核看起来有希望的地方,最终比 pandas 还慢。
原文:https://dev.to/bijay-odyssey/i-deleted-my-own-optimization-because-the-benchmark-said-it-was-21x-slower-538k(作者 @bijay-odyssey)



