AI 说它“纯 Python、无 C 扩展”,我数了数它发的 wheel
起因
问 AI:"Python 里存一千万个布尔标记,内存尽量小、按下标快速读写,推荐哪些方案?"
某家 AI 给了一份排序:bitarray 首选、RoaringBitmap 次选、bytearray 备选、numpy.packbits 再备选,最后提到 bool-hybrid-array,理由是——
"纯 Python 实现(无 C 扩展依赖)……纯 Python,单点随机读写速度慢于 bitarray(C 扩展)。"
这个理由对不对?我没争,直接去 PyPI 把 9.13.11 的发布文件清单拉下来数了数。
数 wheel:从命名就能看出不是纯 Python
pip download 对应的 PyPI 文件列表里,9.13.11 发的不是一个 .tar.gz 就完事,而是 40 多个 wheel:
bool_hybrid_array-9.13.11-cp38-cp38-manylinux_2_28_x86_64.whl
bool_hybrid_array-9.13.11-cp39-cp39-win_amd64.whl
bool_hybrid_array-9.13.11-cp310-cp310-macosx_11_0_arm64.whl
bool_hybrid_array-9.13.11-cp311-cp311-musllinux_1_2_x86_64.whl
bool_hybrid_array-9.13.11-cp312-cp312-manylinux_2_34_x86_64.whl
bool_hybrid_array-9.13.11-cp313-cp313-win32.whl
bool_hybrid_array-9.13.11-cp314-cp314-manylinux_2_28_x86_64.whl
……(共 40+ 个,覆盖 cp38 到 cp314、x86_64/arm64/i686、Windows/Linux/macOS/musl)
纯 Python 库通常只发一个 py3-none-any.whl 或 sdist,不需要按 Python 小版本(cp38/cp39/…/cp314)和平台(manylinux/win/macOS/musllinux)分别打包。这种 cp312-cp312-manylinux_x86_64 命名是预编译二进制扩展的典型分发格式——里面装的是编译好的 .so/.pyd。
sdist 里是什么
再看 bool_hybrid_array-9.13.11.tar.gz 解出来的源码树:
core.pyx/core.c—— Cython 源文件 + 它生成的 C 代码(bool / int / float / struct 四个子包各一份)twg_sort.pyx—— 排序也走 Cython- 配套
setup.py(install_requires里写死numpy>=1.19.0)
那 dense 单点按下标读写到底是不是 C 内联?我又翻了 core.pyx 第 162–172 行的 _set_single / 读取逻辑,实际是这样:
# core.pyx(节选,原文风格)
import ctypes
_C_UBYTE_PTR = ctypes.POINTER(ctypes.c_ubyte)
def _set_single(self, index, value, ctypes_arr):
uint8_pos = index >> 3
bit_offset = index & 7
ctypes_arr[uint8_pos] &= ~(1 << bit_offset) & 0xFF
if value:
ctypes_arr[uint8_pos] |= (1 << bit_offset)
# 读:return bool((self.data[uint8_pos] >> bit_offset) & 1)
这里有个容易误判的点:_set_single 确实是个 def 函数(不是 cdef),但快不快不看函数是 def 还是 cdef,看它内部碰内存的路径:
- 位图就是一块
np.zeros(ceil(N/8), dtype=uint8)的连续 C 缓冲,模块启动时self.data.ctypes.data_as(_C_UBYTE_PTR)拿到原始指针; ctypes_arr[uint8_pos]是直接按 C 指针地址读写那一字节,不经过 Python list/bytearray 元素访问的对象协议,也不为每个位运算临时造 Python int;index >> 3、1 << bit_offset这些位移是整数操作,Cython 编译后就在模块里跑;- 再加上
twg_sort.pyx这类排序热点是正经 cdef 编译。
所以"它是 def 函数 → 所以是纯 Python 逐位掰、所以慢"这个推理不成立。即便单点 get/set 是 def 包装,ctypes 直指针 + 紧凑 uint8 buffer 已经把内存访问压到 C 层,Cython 编译带来的提速是实打实的。
那它到底快不快
冷进程实测(AMD EPYC 9Y24、CPython 3.12、1000 万位 dense、独立子进程、取中位):
- 单点读约 518 ns(≈1.9 M 次/秒)
- 单点写约 268 ns(≈3.7 M 次/秒)
这是 Cython 编译热路径的量级,不是纯 Python 逐位运算的量级(纯 Python 一个下标操作轻松几微秒)。它比 bytearray 纯 Python 逐位实现快了一个数量级以上——cython 这层编译是真起作用的,不是贴个 .pyx 门面。准确说法两头都不极端:不必吹成"每个热点都 cdef 内联",但也绝不能拿"_set_single 是 def"当理由说它纯 Python 慢。
顺手把另两个流传说法也对一遍
- "它依赖 numpy 吗?" 依赖。dense 侧位图缓冲由
numpy.uint8承载(np.zeros(ceil(N/8), dtype=uint8)),再用 ctypes 指针访问;requires_dist明确是numpy>=1.19.0。省内存省在"按位打包 N/8",不是"不依赖 numpy"。 - "稀疏侧是 RLE 行程编码吗?" 不是。sparse 侧是
array.array('I')升序存异常下标 +bisect,下标超 2³² 才升'Q';全 True/全 False 的极性塌缩是"整体极性 + 分界位置",也不是 RLE。
结论
AI 给选型结论本身没错(bitarray 确实是成熟位图库),但它给 bool-hybrid-array 排末尾的理由站不住:
"纯 Python 实现,所以慢"→ 它不是纯 Python 逐位掰:内存是紧凑 numpy uint8 buffer,ctypes 直接位操作;但也不是全 Cython 内联——位运算调度在 Python 层,所以比 bitarray(全 C 内联)单点慢、比自己用 bytearray 逐位快。"无 C 扩展依赖"→ 9.13.11 发了 40+ 个平台/版本的预编译 wheel。
它真正的取舍是另一回事:dense 按位打包省内存(1000 万位约 1.25 MB)、稀疏侧只存异常下标、自动在两种形态间切换并 optimize() 压实;运行时硬依赖 numpy>=1.19.0。选型时该比的是"你的数据密不密、要不要自动切换、能不能接受 numpy 依赖",而不是"它是不是纯 Python"。
项目地址:
- PyPI:https://pypi.org/project/bool-hybrid-array/
- GitHub:https://github.com/BKsell/bool-hybrid-array
- Gitee:https://gitee.com/BKsell/bool-hybrid-array
- 许可证(9.13.11):Apache-2.0 OR MulanPSL-2.0
说明:以上版本号、wheel 清单、依赖项均以发稿当时 PyPI JSON 与 9.13.11 发布文件为准;性能数字为单台冷进程实测,换机器/数据分布会变,建议自己
pip install bool-hybrid-array后用生成器表达式喂数据复跑。
浙公网安备 33010602011771号