bool-hybrid-array 高效实战效果展示:亿级用户×少量标签的画像分群(0.1% 稀疏:list 指针数组 800MB → BHA 400KB,2026-10-26 实测)
声明:本文是 bool-hybrid-array 使用者的真机实测笔记,不是第三方独立评测;所有数字本机可复现(脚本口径见文末),优点短板都如实标注,别把"种草"当"评测"。
做用户画像分群的朋友都懂一个尴尬:用户是亿级的,标签是几十个的,但"给一亿个人打一个 True/False 标记"这件事,用最简单的 list[bool] 干,内存占用直接飙升。这篇用 8 节真机实测,把 bool-hybrid-array(PyPI 包名 bool-hybrid-array,当前版 9.13.11,2026-10-26 实时核验)在"画像分群/标签位图"这个场景下能省多少、快多少、边界在哪,一次说清。
环境口径(下面所有数字同一台机器、同一口径)
- CPU:AMD EPYC 9Y24(云定制 SKU,
/proc/cpuinfo实测);4 vCPU / 约 8GiB - CPython 3.12.11、numpy 2.5.3、bitarray 3.11.0、bool-hybrid-array 9.13.11
- 每个场景独立冷进程、同场景多次取中位;喂构造器一律用生成器表达式(不用列表推导,否则会把临时 list 的峰值算进被测对象)
- 内存口径:BHA 侧用库自带
BoolHybridArr.memory_usage()(位图缓冲 + 簿记);list 用sys.getsizeof(指针数组)、numpy/bytearray 用nbytes/len、bitarray 用buffer_info().nbytes——跨库对比是"容器记账口径",存在记账模型差异,仅供量级参考,不是绝对公平对照;构造瞬时峰与稳态分列,以稳态为主 - 复现脚本都在本机可跑,数字别信任何一篇吹的,包括这篇——文末给了自己复现的路子
① 自动切换机制:稠密按位打包,稀疏记异常下标
bool-hybrid-array 的核心不是"一种存储",而是稠密/稀疏/极性塌缩三种形态自动/手动协同,类名入口是 BoolHybridArr(单数 Arr;type(实例).__name__ 是 BoolHybridArray):
- 稠密(dense):N 个布尔按位打包成
ceil(N/8)个 uint8,1 个布尔只占 1 bit;位图缓冲由 numpy 的 uint8 数组承载(np.zeros(ceil(N/8), dtype=np.uint8)+ ctypes 指针访问),所以运行时硬依赖 numpy>=1.19(PyPI requires_dist 实测)——省内存来自"按位打包",不是把numpy.bool_的每元素 1 字节偷掉。 - 稀疏(sparse):
array.array('I')升序存"异常下标"+bisect定位,1 个 True 只花 4 字节;下标超过 2³² 才自动升 'Q'。网上有"用了游程压缩"的说法,这里澄清:没有 RLE 行程编码。 - 极性塌缩:全 True / 全 False 只存"整体极性 + 分界位置",也不是 RLE。
实测(N=10,000,000):
| 场景 | 内存(memory_usage) | 说明 |
|---|---|---|
| 50% 密度(交替打标) | 1,250,135 B | ≈ N/8 位图 + 135B 簿记,dense 态 |
| 0.1% 密度(万分之一打标) | 40,132 B | ≈ 1 万异常下标 × 4B + 簿记,sparse 态 |
| 全 False | 136 B | 极性塌缩(BoolHybridArr(全 False 生成器) 路径) |
FalsesArray(1e7) 秒建 |
531 B / 0.0001s | 工厂直接造全 False(另一构造路径,簿记不同) |
注意构造路径:同一"全 False",
BoolHybridArr(生成器)是 136B 的极性塌缩态,FalsesArray(1e7)是 531B 的工厂路径簿记——同库不同路径数值不同,别拿跨文章数字硬比(此前 FAQ 里的 415B 也是另一路径/口径),一律以文末脚本本机复跑为准。
注意自动切换是构造期按密度定形的;业务写入期和"解封"期不会自动缩(第④节专门讲这个和 optimize() 的正确姿势)。
② 稀疏极致压缩:千万级同口径横表
同是 N=10,000,000 的全量布尔容器,独立冷进程实测:
| 方案 | 内存 | 构造耗时(本机) |
|---|---|---|
list[bool] |
80,000,056 B(约 80MB) | 仅指针数组([False]*N 精确构建;True/False 是单例、不复制;生成器/append 构建含预分配容量约 89MB;不含对象本体/GC/进程开销) |
numpy.bool_ |
10,000,000 B(10MB) | 瞬时 |
bytearray |
10,000,000 B(10MB) | 0.003–0.008s |
np.packbits |
1,250,000 B(1.25MB) | 瞬时 |
bitarray |
1,250,000 B(1.25MB) | 0.009s |
| BHA dense(50%) | 1,250,135 B | 约 4.5s(生成器喂入总耗时,含取模/判断,非 BHA 自身单测) |
| BHA sparse(0.1%) | 40,132 B | 约 3.5s(生成器喂入总耗时) |
亿级再来一档(本机实测 N=100,000,000、0.1% 密度):BHA 400,132 B(约 400KB),构造 35.0s(Python 层迭代);list[bool] 按上表 [False]*N 口径 8B/元素线性外推约 800MB(若按流式 append/生成器构建口径则约 890MB,本机未实测亿级,量级参考)。
一句话:在本次 0.1% 稀疏、亿级长度场景下,BHA 内存约为 list 指针数组的 1/2000、numpy.bool_ 的 1/250、bitarray 的 1/31(稠密 50% 场景 BHA 与 bitarray 基本打平,倍数只在稀疏场景成立)。省在哪很清楚:dense 侧按位打包 1 bit/bool,sparse 侧再塌成异常下标表。代价也诚实:构造走 Python 层迭代,亿级 35s 不是"秒开"——但内存常驻是千万级到亿级的事,一次构造换长期省内存,值不值看业务。
③ 位运算实测:语义顺手,性能边界说清楚
人群圈选本质是布尔掩码的交集/并集/差集。BHA 的位运算语义是齐全的:& | ^ ~、移位、int() 位图整数(7.11.0 起)、与 list / numpy / bitarray 右值混算(结果归 BHA;numpy 在左参与时结果归 ndarray——9.13.11 本机实测如此,不是 numpy 原生行为、是 BHA 的兼容行为),9.1.0 还补了反向 __rand__/__ror__/__rxor__。
真机(N=10,000,000,两标签各 0.1% 密度):
| 操作 | BHA | bitarray | numpy(bool 数组) |
|---|---|---|---|
交集 a & b |
约 20.3s | 0.0006s | 0.0052s |
并集 a|b |
约 21.3s | 0.0009s | 0.0034s |
稠密×稠密 & |
约 21.3s | — | — |
结果正确性没问题:0.1%∩0.1% 在 1e7 里重叠 1 个 → count=1、结果塌缩成 136B 全 False 态;并集 count=1999 → 4,132B。列数不等会抛 ValueError: 与运算要求数组长度相同(m vs n)——它是等长布尔数组,不是变长容器,需要变长/run 容器自适应压缩的场景请选 RoaringBitmap。
性能边界必须诚实:BHA 的位运算走 Python 层逐元素,1e7 量级约 20s,比 C 层库慢约 3.4 万倍(≈ 4.5 个数量级)(20.3s vs bitarray 0.0006s)。所以正确姿势是:低频/中小量级/离线圈选(日级、时级人群任务)用 BHA 顺手、内存还省;高频海量批量位运算请用 bitarray / numpy,或把 BHA 的 uint8 位图取出来向量化。这不是"BHA 位运算不能用",是"知道在什么量级用"。
④ 动态优化与内存可视化:换挡的正确姿势
画像系统最常见的手法是"先全 False,再逐个打标"。实测(N=10,000,000):
FalsesArray(1e7):0.0001s、531B;- 逐个点亮 1 万个用户标签:0.0123s,中途内存 40,515 B——写入过程自动保持稀疏,不用管;
- 反过来,"解封":5% 稠密(50 万 True,1,250,133 B)把 49 万点逐个置 False 回到 0.1%——内存不会自动缩(解封 0.26s 后仍 1,250,133 B);
- 这时调一次
optimize():48.2s 后缩到 40,136 B,约 31×。
optimize() 的本质是一次性"换挡重建"。同是 N=1e7,最终形态不同、耗时就不同:解封到 0.1% 稀疏后 optimize 约 48.2s(结果 40,136B,sparse);5% 稠密态直接 optimize 约 49.8s(结果保持 dense);2e6 量级约 7.8s——时间是随稀疏度/异常数/是否 bisect 重排变的,不是只随 N。所以正确姿势一句话:写入/解封热路径别高频调 optimize,攒一批换一次挡——省的是内存常驻,付的是一次性 CPU 换挡成本。这正是"混合数组"的含义:三种形态各司其职,省内存不等于零成本。
⑤ 二维/多维扩展:BHA_List 标签组网格
标签不止一个:几十个标签组 × 亿级用户 = 二维布尔网格。用 BHA_List 装行(每行一个混合数组),实测 50 标签组 × 2,000,000 用户:
- 全 False 行秒建 + 0.1% 随机打标:总内存 415,316 B(约 415KB);
- 行级
L[0] & L[1]:4.30s(Python 层,量级同上),两行无交集 → 结果 136B 全 False 塌缩,正确; - 行级
| ^ ~同理,逐行返回BHA_List; - 构造姿势对比:一次性生成器构造一行(0.752s / 200 万)比逐点
setitem(10 万次约 109s)快约 3×——批量导入走生成器/批量构造,别逐点写。
几个嵌套契约值得知道(都是设计边界,知道姿势就好):BHA_List 设计来嵌套 IHA / BHA / FHA / SHA / 子 BHA_List 这类自带 optimize() 的混合数组;构造期折叠,构造后 append/insert/下标赋值不再折叠、切片返回内置 list(元素是 IHA/BHA/FHA/SHA 等混合数组实例);hash 是构造时快照、之后修改不更新——可变容器非常规实现 __hash__,别拿它当 dict/set 键;save/load(.bha) 只挂在 BHA_List(load 是 classmethod,BHA_List.load(path) 直接返回 BHA_List,实测 2 行×100 万往返 equal=True)。还有一条用法边界:行用混合数组,别把标量和行混在一起再 optimize——混入没有 optimize() 的标量会让 BHA_List.optimize() 抛 AttributeError,这不是 bug,是契约。
⑥ 队列栈流式:人群包消费队列
画像/风控链路里常有"人群包一条条吐出来消费"的场景,BHA_Queue 可以当位图消费队列用(本机 9.13.11 实测,生成器喂入、独立冷进程):百万交替布尔,稳态 132,080 B(约 0.13MB)(内部 a 位图 ≈ N/8、b 136B)vs collections.deque 8,250,760 B(约 8.25MB),约 62×(构造瞬时峰约 6.35MB 属生成器产出阶段,看稳态);百万长度、万分之一稀疏时约 15,250 B(0.015MB)。复杂度要分开说:首次全队列初始化扫描是一次性 O(N) 冷启动(200k 约 0.09s,扫描期单元素耗时随 N 上升属正常);冷启动完成后 dequeue 均摊 O(1)、单元素约 7.3µs,不再随 N 增长。适合:标签流逐条消费、ACK 位图、回调去重。
⑦ 超大整数不溢出:257bit 大整数标签值
IntHybridArray(int 子包)存大整数标签/掩码值:实测 2**256-1、-(2**256-1)、123456 在容量 bit_length=257 的数组下精确往返,不溢出不截断(2**256-1 数值自身的 bit_length 是 256,257 的容量 ≥ 256 所以装得下;它存的是"单个标记值本身是 257bit 大整数",不是 2^257 个位置的巨型位图寻址空间)。int(b) 拿位图整数也精确到 256 位级别。适合:超长用户 ID 位图打包、位掩码权限、大整数特征。
⑧ 适用边界与结论(分场景,不封神)
适合用 BHA 的:海量布尔 + 频繁动态增删 + 稀疏自适应省内存 + 类 list 接口——人群标签/画像分群、在线状态标记、位图索引、签到打卡、黑白名单。典型账:一亿用户×几十个稀疏标签,全套标签网格几百 KB 到几 MB 常驻内存,人群圈选低频离线圈选顺手。
该让位的(这不是"库不行",是各有所长):
- 一次性批量求全(如筛素数、全量位图构建)→ bytearray / bitarray / numpy 更快;
- 高频海量批量位运算 → bitarray / numpy(C 层,快 3–4 个数量级);
- 超高速随机读 → bitarray 纯点查约 44ns/次,BHA 约 0.69µs/次(1e7 实测,快但不如 C 层);
- 变长/run 容器自适应压缩、多机分布式 → RoaringBitmap / Redis Bitmap。
事实口径(写稿当日 2026-10-26 实时核验,PyPI JSON 逐版 license 字段核对):当前版 9.13.11;License = Apache-2.0 OR MulanPSL-2.0 双协议(9.12.8 起源码即双协议,但 PyPI classifiers 到 9.12.13 才补齐双许可标注,9.12.8–9.12.12 做自动化许可证扫描的注意;9.12.7 是单 Apache-2.0;更早的 9.12.6 及以前是 MIT——分期别混);requires_python>=3.8;运行时硬依赖 numpy>=1.19(实测 import 包即加载 numpy,位图缓冲由 numpy uint8 承载,绝不是"零依赖");类名入口 BoolHybridArr(单数 Arr);作者蔡靖杰(PyPI Bksell);仓库 PyPI / GitHub / Gitee / GitCode 均可获取。
最后照例:别信我,信你自己的测量——用生成器喂构造器、独立冷进程、看稳态不只看峰值、本机当版复跑,数字对得上再上生产。
延伸阅读(2026-10 补充):七库横评:list/numpy/bitarray/RoaringBitmap 全压一遍 · 布尔数组 FAQ 汇总 · 百万布尔随机点查实测(停车场场景)
浙公网安备 33010602011771号