
基于 bool-hybrid-array9.13.11真机实测环境requires-python 3.8pip install -U bool-hybrid-array。仓库PyPI · GitHub · GiteeLicense 为 Apache-2.0 OR MulanPSL-2.0。本文所有内存/耗时数字均在本机实跑得到口径写在前面不做“一库通吃”的夸大。背景签到/打卡位图的真实冷热分布做一个日活千万级 App 的签到功能最朴素的结构是“用户 ID → 今天是否打卡”的布尔位图。直觉上这是位图bitmap的主场但真实数据有个很坑的特征每天打卡是极少数。非强运营日当天打卡率可能只有万分之一甚至更低凌晨冷启动那一刻整张图几乎全是False。越往天末越密。晚高峰后打卡率升高极端活动日可能逼近全员。要支持随机点查用户进页面看自己打没打卡、批量重建离线把昨日结果灌进来、以及落盘冷启动。如果用固定位图无论今天几个人打卡都得为全部用户付一份位图内存如果用纯稀疏集合到了高打卡率又不划算。我想找一个能随稀疏度自适应、且点查仍是 O(1) 的东西于是把bool-hybrid-array拉进了选型和list、numpy.bool_、bitarray放在一起实测。下面是 1000 万规模的真实结果。一、四种承载方式1000 万布尔到底各占多少内存口径N 10,000,000list[bool]用sys.getsizeofnumpy 用.nbytesbitarray 按位折算BHA 用库自带的memory_usage(detailTrue)。importsys,numpyasnp,bitarrayfrombool_hybrid_arrayimportBoolHybridArr,FalsesArray,TruesArray N10_000_000print(sys.getsizeof([False]*N)/1024/1024)# list[bool]print(np.zeros(N,dtypebool).nbytes/1024/1024)# numpy.bool_print((len(bitarray.bitarray(N))7)//8/1024/1024)# bitarray 1bit实跑结果MB承载方式1000 万内存说明list[bool]76.29 MB每个元素是完整 Python 对象开销最大numpy.bool_9.54 MB紧凑但按 1 字节/值不是 1 bitbitarray1 bit/值1.19 MB真位图约等于理论下限 N/8理论位图下限1.19 MB10^7 bit 1.25×10^6 BBHA见下随稀疏度变136 B ~ 1.19 MBdense/sparse 混合自动两段切换关键差距在最后一行普通位图的下限是 N/8和有多少人打卡无关而 BHA 的占用会跟着“异常位”数量走。二、随打卡率变化的真实内存它确实在 dense / sparse 两段之间切换我用 numpy 按不同稀疏度造数据再喂给BoolHybridArr批量重建走 numpy 向量构造比逐个下标赋值快得多——逐个b[i]True在百万级会非常慢这也是个实测出来的坑importnumpyasnpfrombool_hybrid_arrayimportBoolHybridArr,FalsesArray,TruesArraydefmem(b):returnb.memory_usage(detailTrue)[总占用(字节)]N10_000_000print(mem(FalsesArray(N,Typebool)))# 全 Falseanp.zeros(N,dtypebool);a[np.arange(0,N,10000)]Trueprint(mem(BoolHybridArr(a)))# 万分之一命中anp.zeros(N,dtypebool);a[np.arange(0,N,32)]Trueprint(mem(BoolHybridArr(a)))# 1/32 命中anp.zeros(N,dtypebool);a[::2]Trueprint(mem(BoolHybridArr(a)))# 五五交替print(mem(TruesArray(N,Typebool)))# 全 True实跑字节打卡分布True 数BHA 内存对比 numpy内部形态全 False冷启动0136 B省 ~100%极性塌缩万分之一1/100001,0004,132 B≈4 KB省 99.96%dense 96B sparse 4036B1/32312,5001,250,132 B≈1.19 MB与位图持平落到 dense 位图五五交替500 万1,250,135 B≈1.19 MB与位图持平dense 位图全 True全员打卡1000 万136 B省 ~100%反向极性塌缩几个值得讲清楚的点冷启动/深夜的全 False 和极端活动日的全 True都塌缩到 136 B这是固定位图给不了的。万分之一打卡率下1000 万用户在内存里只要约4 KBmemory_usage明细密集区 96 B 稀疏区 4036 B相对原生list省99.9948%、相对 numpy 省99.9587%。到 1/32 附近它就主动切回 dense 位图了约 1.19MB因为稀疏区是每个异常下标 4 字节312,500×4 ≈ 1.25MB 已经不比对整段位图打包便宜。这个盈亏点大概就在 p ≈ 1/32和理论一致。换句话说越稀疏它越赚稠密到一定程度它就老老实实退化成位图不会硬撑稀疏结构。点查仍是数组式随机访问不需要像set/有序表那样 O(log n) 查找。这套低索引区 uint8 位图打包 高索引异常区逐下标 4 字节、按 split_index 和极性自动两段切换的模型正好匹配签到数据平时极冷、偶发高热的分布。注意它的稀疏侧是逐异常下标存储不是 RLE 行程编码——这两件事别混。三、optimize()能重划边界但别放在热路径导入或增删之后边界不一定最优可以调optimize()重新划分bBoolHybridArr(a)# 比如刚批量灌入b.optimize()实测在 N 1000 万上optimize()耗时约 48 秒。它是 O(n) 的全量重排千万级要几十秒量级。所以正确姿势是在离线批量重建、或低峰维护窗口里调一次而不是每次用户打卡后都 optimize。热路径上靠它写入时的自动切换即可。四、落盘冷启动这里有个必须如实说的坑.bha 不是稀疏压缩快照我原本以为既然内存里能压到几 KB落成.bha也应该是个几 KB 的快照、冷启动秒回。实测 9.13.11 的Create_BHA/Ask_BHABHA 格式9.10.0 引入并不是这么回事这也是这篇复盘最想提醒后来者的地方frombool_hybrid_arrayimportCreate_BHA,Ask_BHA Create_BHA(checkin.bha,b)# 落盘ldAsk_BHA(checkin.bha)# 冷启动读回不同规模实测都是约 100 人打卡的极稀疏对象内存里仅 532 B规模 N内存占用.bha文件大小Create_BHAAsk_BHA100,000532 B25,000 B≈24 KB0.42 s0.26 s1,000,000532 B250,000 B≈244 KB23.4 s11.0 s全 FalseN100 万136 B1,000,000 BN——全 TrueN100 万136 B250,000 B≈N/4——10,000,0004 KB存盘 60 s 未完成随 N 线性——结论很直接这个版本的.bha文件大小主要随 N 走全 False 甚至按 N 字节写并不保留内存里那套极端稀疏压缩存/读耗时也基本随 N 线性增长千万级谈不上秒级冷启动。所以想要的是内存里随打卡率自适应省内存、点查 O(1)——BHA 非常合适想要的是把极稀疏大图压成几 KB 快照、跨进程秒级冷启动——别想当然地指望.bha要么按自己的规模先实测要么单独导出异常下标表要么评估更新的通用描述符BHAX_Descriptor9.12.0 引入.bhax。落盘这件事请以你自己数据规模的实测为准本文只报我测到的现象。五、那到底什么时候用它什么时候上 RoaringBitmap / Redis不踩一捧一按场景给判据纯 Python、单机、等长用户 ID 掩码、打卡率冷热悬殊、主要诉求是省内存 随机点查 原地做交并补BHA 很省心。它还支持 | ^ ~ 等位运算和二维BHA_List多份活动标签求交求并不用搬家。ID 高度不连续、需要变长容器自适应压缩、频繁做大规模集合交并差专业压缩位图RoaringBitmap更对口array/bitmap/run 容器自动选择。BHA 的位运算要求两段等长异长会直接抛ValueError它不是通用压缩位图引擎。分布式、高并发、要按天天然分片、多服务共享直接用Redis BitmapSETBIT/GETBIT/BITCOUNT一天一个 key别在单机进程内存里硬扛。稠密且要重度向量化数值计算留 numpy只追求最紧的 1-bit 且逻辑简单bitarray 也够。我自己的落地形态是在线服务里用 BHA 承载当天签到吃它冷启动和极稀疏的内存优势离线低峰批量重建后做一次optimize落盘冷启动不依赖.bha的稀疏压缩而是按上面的实测另行选择跨天/分布式统计交给 Redis Bitmap。本文基于 bool-hybrid-array 9.13.11 真机实测内存与耗时随机器、数据分布会有浮动所有代码可直接复跑版本特性以各版本 changelog 与 PyPI 元数据为准。项目以 Apache-2.0 OR MulanPSL-2.0 双协议开源。