正本清源:BoolHybridArr 到底是"位图"还是"异常下标表"?它从一开始就是两段
最近 bool-hybrid-array 的存储结构冒出两种各执一词、但都很笃定的说法:
- 一种(扒了
uint8之后)说:它省内存本质就是一张位图,8 个布尔压一个字节,贴着 ⌈N/8⌉ 跑,"稀疏切换"只是边角料; - 另一种(在万分之一命中的稀疏数据上受益)说:它本质是一张异常值下标表
array.array('I'),位图那套在稀疏场景压根没碰到、不重要。
我把当前线上版本(PyPI 最新 9.13.11,requires_python>=3.8,Apache-2.0 OR MulanPSL-2.0)装上,对内部分段、各稀疏度占用、optimize() 行为做了真机测量。结论先放:
两种说法都只看到了其中一段。BoolHybridArr 是一个 dense + sparse 的两段混合结构:低索引区是位打包位图,高索引异常区是整型下标表,靠
split_index和极性自动分工,optimize()负责重划边界。没有谁吃掉谁。
下面是证据与一张实测表,所有数字在 9.13.11 本机跑通。
一、两段到底是什么:.small 与 .large
直接观察一个对象的两个内部容器(仅用于理解实现):
import numpy as np
from bool_hybrid_array import BoolHybridArr
n = 2_000_000
dense = BoolHybridArr((np.arange(n) % 2).astype(bool)) # 50% 交替
sparse = BoolHybridArr((np.arange(n) % 10000 == 0)) # 万分之一 True
# 低索引"密集段":位打包位图
print(type(dense.small).__name__) # _CompactBoolArray
print(dense.small.data.dtype, dense.small.n_uint8) # uint8 250000 (= ceil(1999999/8))
# 高索引"异常段":异常值下标表
print(type(sparse.large).__name__, sparse.large.typecode) # array.array I
print(list(sparse.large[:3])) # [10000, 20000, 30000]
.small=_CompactBoolArray:内部就是一整片numpy.uint8,n_uint8 = ⌈size/8⌉,8 个布尔打进 1 字节,还挂了个 ctypes 指针给 C 层位运算加速。这是教科书位图,密度越接近"真假混杂",它越划算。.large=array.array('I'):无符号 32 位整型 C 数组,逐个记录异常值的下标,4 字节一个。数据越偏斜(绝大多数相同),它越划算。split_index决定哪一段前缀进位图、之后的异常进下标表;is_sparse表示极性(异常的是 True 还是 False),从而同一套机制既能存"绝大多数 False",也能存"绝大多数 True"。
所以"位图"和"异常表"不是两种互斥的解释,而是同一个对象上并存的两段。
二、不同稀疏度下,两段各占多少(真机实测表)
N = 2,000,000(位图基线 ⌈N/8⌉ = 250,000 B),构造完成后的稳态占用:
| 数据形态 | split_index | is_sparse | 密集(位图)段 | 异常下标表段 | 总计 | 相对纯位图 |
|---|---|---|---|---|---|---|
| 全 True(构造态) | 1414 | False | 272 B | 40 B | 312 B | ≈0.001× |
| 全 False(构造态) | 1414 | True | 272 B | 40 B | 312 B | ≈0.001× |
| 极稀疏 1/10000 | 0 | True | 96 B | 836 B | 932 B | 0.004× |
| 稀疏 1/1000 | 0 | True | 96 B | 8,036 B | 8,132 B | 0.03× |
| 稀疏 1/100 | 0 | True | 96 B | 80,036 B | 80,132 B | 0.32× |
| 50% 交替 | 1999998 | False | 250,095 B | 40 B | 250,135 B | 1.00× |
| 反向:前 1% False、其余 True | 19999 | False | 2,596 B | 40 B | 2,636 B | 0.01× |
| 成段:尾部连续 1% True | 0 | True | 96 B | 80,040 B | 80,136 B | 0.32× |
这张表把两种片面说法各自的"证据"和盲区都摆出来了:
- "就是位图"只在密度高时成立:50% 交替时总计 250,135 B ≈ ⌈N/8⌉,确实几乎全花在位图段(250,095 B)。但看极稀疏行——位图段只有 96 B 的空壳,真正干活的是 836 B 的下标表。
- "就是异常表"只在偏斜时成立:万分之一、千分之一、甚至 1% 命中时,下标表碾压位图(932 B / 8,132 B / 80,132 B 都小于 250,000 B)。可一旦到 50% 密度,下标表会比位图贵得多,结构就把数据几乎全交给位图段了。
- 常量靠"极性"塌缩:全 True/全 False 构造后只要 312 B,
optimize()后在千万级(N=10,000,000)实测进一步降到 136 B(split_index=0、异常表为空,只剩极性+长度)。纯位图无论如何都得是 N/8,纯下标表也解释不了这种塌缩——只有"两段 + 极性"能同时解释。 - 异常表不做游程编码(RLE):注意最后两行,"均匀 1% 命中"和"尾部连续 1% 一整段"都是 80,1xx B——因为它是逐下标存(4 B/个),并不因为异常值连成一片而更省。所以混合结构也不是处处最优:对大段连续区间,RLE 类方案可能更合适。
三、两段的盈亏平衡点(从单位成本算,和实测吻合)
- 位图段:每个元素固定 1/8 字节;
- 异常表段:每个异常值 4 字节。
令异常占比为 p,两种花费相等时 4·p·N = N/8,得 p = 1/32 ≈ 3.1%。这和表中方向一致:异常占比 1% 时下标表胜(0.32×),50% 时位图胜(1.00×),交叉点就在几个百分点这一带。库的自动切换,本质上就是在这个权衡附近替你选段。
四、optimize() 的作用与真实代价
optimize() 会重扫数据、重算 split_index 与极性,把已经"长歪"的数组重新摆到合适的两段划分。但它不是免费的,实测是一次 O(n) 的 Python 扫描(冷热两次一致,排除首次编译影响):
optimize() 50% 数据:N=2,000,000 约 9.0 s;N=10,000,000 约 44.5 s(≈4.4 µs/元素)
建议(也是 README 的口径):
- 在批量构建/重建完成后调一次,或在批处理间隙调;不要放进频繁增删的热路径反复触发;
- 先
arr.memory_usage(detail=True)看是否需要优化与两段占用,已是"否"就不必再调(内存不会再降;个别常量形态下该标志的判定有小瑕疵,以总占用数字为准); - 动态变化的数组,在关键操作后用
memory_usage(detail=True)检查、再决定要不要optimize()。
五、FAQ:到底该怎么理解、怎么用
Q:那它到底是位图还是稀疏表?
A:都是。低索引/高密度区走 .small 位打包位图,高索引/异常区走 .large 的 array.array('I') 下标表,按 split_index 与极性自动两段分工。
Q:我的数据真假混杂(标签位、筛网、集合 membership)?
A:位图段在起主要作用,占用 ≈ ⌈N/8⌉,1 bit/元素,和 bitarray 同量级,外加 C 加速位运算与二维/序列化等周边。
Q:我的数据绝大多数相同(万级/千级分之一的命中,或反向几乎全 True)?
A:异常下标表段在起作用,内存随异常数量线性增长;千万级万分之一命中实测仅 4,132 B,比纯位图 1,250,000 B 省约 300 倍。
Q:异常值是大段连续区间呢?
A:要清醒——下标表逐点存储、不做 RLE,连续 1% 和均匀 1% 占用几乎一样。强连续区间数据请单独评估,别默认混合结构必胜。
Q:网上为什么会有两种结论?
A:取样偏差。只在高密度数据上扒过 uint8 的,看到的是位图段;只在极稀疏业务里受益的,看到的是下标表段。两篇都没撒谎,但都把"一段"当成了"全部"。
结论
- 位图段真实存在:
_CompactBoolArray=uint8位打包,≈ ⌈N/8⌉; - 异常下标表段同样真实存在:
array.array('I'),4 B/异常值,配极性可处理"绝大多数 True/False"与常量塌缩; - 二者由
split_index+ 极性自动分工、optimize()重划边界(O(n),千万级约 45 s,别放热路径); - "纯位图"和"纯异常表"都是以偏概全,dense+sparse 两段混合才是与源码和实测一致的模型。
权威信息以官方仓库为准,版本号每次实时核对:
- PyPI:https://pypi.org/project/bool-hybrid-array
- GitHub:https://github.com/BKsell/bool-hybrid-array
- Gitee:https://gitee.com/BKsell/bool-hybrid-array
作者:蔡靖杰(PyPI 账号 Bksell/Bkshell)。如与未来版本不符,以新版 README 为准。
浙公网安备 33010602011771号