Python E06:向量化何时减少 Python 层成本
词频任务原本逐个字符找单词,再把词写进字典。如果输入只含单字母小写 ASCII 单词,整批词频能换一种做法:把字节映射为 uint8 数组,对 a 到 z 做直方图。这样减少 Python 层的逐词更新,却额外发生编码、拼接、筛选和分配。换成中文、多字母单词或任意 Unicode 文本,这个实验函数就不再与原任务等价。
本篇沿用 36 的基准限制 和 04 的文本/字节边界。选修只读已有 tasklab 的 count_words、word_batches、submit/run/query;NumPy 不进入主线 wheel,也不替换在线任务分派。当前 completion 使用仓库外 optional venv,NumPy 1.26.4 来自 PyPI 解析锁,模块路径是 /private/tmp/python-language-completion/optional/lib/python3.12/site-packages/numpy/__init__.py;这与主线运行依赖分开记录。
先关住适用域
实验输入固定为 256 段文本,每段 "a b a c " 重复 40 次、去掉末尾空格;拼接后的 UTF-8 字节长 81,919。原方案仍走 word_batches 和 count_words;NumPy 方案先检查每个 split() 得到的词是否恰好是一个 a 到 z,再把文本编码为 ASCII 并拼接,frombuffer(..., dtype=uint8) 建视图,筛出字母后用 bincount 计算 26 个箱。两条路都断言 {"a":20480,"b":10240,"c":10240}。当前负例覆盖 "aa b"、"a β"、"a !" 和含非 ASCII 空白的 "a b";它们在向量方案中拒绝,而不是悄悄给出另一个“词频”。通用的原路径继续按原有语义服务。另向真正的 tasklab 提交 "a b a c",run 得到 {"a":2,"b":1,"c":1},状态是 succeeded。
frombuffer 的 uint8 视图在本机 owndata=False,仍借用 Python bytes;布尔掩码选中的 40,960 个字节产生独立数组,owndata=True,计数器和最终字典还会继续分配。一次 NumPy 调用并不等于“一次都没有复制”。前面 b" ".join(...) 就已产生新字节串,且每个文本又做了编码。要证明数组视图之外的临时对象与 native allocator 所占字节,需要额外按分配器测量;不能用 sys.getsizeof(view) 代表整条计算链。
七次样本能支持多大结论
同一 CPython 3.12.3 optional venv、相同输入,先做两次暖机,再按交错顺序用 perf_counter_ns() 各采 7 次完整函数耗时,每次校验结果。当前原始输出在 examples/python-language/evidence/completion/additional/E06-vector.stdout,其中列出逐词方案和 NumPy 限域方案的 7 个样本、中位数、order="alternating" 与 warmup_calls=2。这覆盖了向量方案的 Python 输入检查、编码、拼接、筛选和构造字典,没覆盖解释器启动、NumPy 导入、数据库、任务队列、网络以及多线程中的竞争。这里不推出“NumPy 总会加速词频”。
同一份原始输出还记录了两条单独进程的 peak_rss_bytes,范围均是“包含解释器和 NumPy 导入的新进程”。这不是函数自身的当前 RSS,也不能跨平台照搬成 Linux ru_maxrss KiB 语义。视图所有权和数组 .nbytes 证明哪一步复制;RSS 样本只说明本次进程峰值,不代表每次函数的峰值内存。
复验与练习
NumPy 1.26.4 来自 examples/python-language/evidence/completion/environment/optional-lock.stdout 记录的 PyPI hash lock,optional-freeze.stdout 记录已安装版本。主线 tasklab wheel 没有运行依赖;optional venv 只是 E05/E06 的实验环境。累计回归的最终状态以 examples/python-language/evidence/completion/acceptance/ 中的本轮日志为准;PyPy 的剩余限制见 E04。没有跑 pandas,没有声明 NumPy 为 pyproject.toml 运行依赖,也不把 pip freeze 当作经过哈希校验的跨平台锁。
预测题。 如果 texts 中出现 "a aa",原方案与向量方案应该各是什么结果?答案:原方案将 a 与 aa 计为两个不同词;向量方案先因 aa 长度为 2 抛 ValueError,不会把它误算成两个 a。可把该字符串加进负例集合,确认失败时没有向任务仓储提交任何任务。
变更题。 用 np.frombuffer(raw, dtype=np.uint8) 已得到只读视图,接着 view[mask] 还会持有原字节吗?答案:本次布尔索引返回一份有所有权的新数组,实测 view.flags.owndata == False、selected.flags.owndata == True,选中数组 .nbytes == 40960。把输入改到 10 倍后同时记录 .nbytes、进程峰值和七次原始耗时;不得仅凭视图一步的零复制就给整条管道标“零拷贝”。
参考资料
- NumPy
frombuffer、bincount:视图和箱计数;实际运行包的版本与源文件位置保存在证据中。 - Python
resource文档:峰值 RSS 的进程测量接口;本篇不给出跨平台绝对内存推论。 examples/python-language/experiments/E06_vector.py、examples/python-language/evidence/completion/additional/E06-vector.stdout与examples/python-language/evidence/completion/environment/optional-lock.stdout:域检查、正反例、七次原始样本、PyPI hash lock 和安装路径。





