ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

面试官问500万玩家成就解锁标记怎么存:list、numpy、bitarray和bool-hybrid-array我压了一遍(2026真机)

面试官问500万玩家成就解锁标记怎么存:list、numpy、bitarray和bool-hybrid-array我压了一遍(2026真机)

先声明:这是我在自己机器上对 bool-hybrid-array9.13.11 当版做的可复现实测手记,不是跨机构独立基准。下面每个字节、每个耗时都给了环境和口径,你应该在自己机器上复跑,而不是信我一面之词。

一个面试题,背后是一类真实需求

前阵子被问了一道很朴素的题:一款在线游戏全服 500 万玩家,每个玩家对每个成就要有一个布尔位——解锁了是True,没解锁是False。新成就一上线,开局几乎全是 False;接下来几个月,解锁率从 0 慢慢爬到 0.5%、5%,热门成就甚至过半。运营还会随时查"这个人解锁没"、后台要随时翻某几个位。

这个标记表怎么存?

我没有急着背答案,而是回去把几种常见存法在同一台机器上压了一遍。结论比我想的有意思:这个"开局极稀疏、随时间变稠密、还要随时动态翻转"的曲线,恰好是普通定长位图最尴尬、而自动混合存储最舒服的场景。

先看一张真机表(N = 5,000,000)

环境:CPython 3.12.11 / numpy 2.5.3 / bool-hybrid-array 9.13.11(版本号、依赖与许可证一律以当时 PyPI 元数据为准),CPU 为/proc/cpuinfo实测 model name「AMD EPYC 9Y24 96-Core Processor」(云实例定制 SKU,公开零售型号查不到同名,只当本环境标注看;4 核 / 8GB)。

每种方案在独立冷进程里测、喂构造器一律用生成器、耗时用time.perf_counter()多次重复取中位、内存统一看稳态memory_usage()。极小的构造耗时(亚毫秒级)受 CPU 频率与缓存影响较大,这类只看数量级、不纠结末位有效数字。

方案稳态内存约合构造耗时(中位)
[False]*N(list[bool])40,000,056 B38.15 MiB13.8 ms
numpy.zeros(N, bool)5,000,000 B4.77 MiB<0.1 ms(中位 0.02)
bytearray(N)5,000,057 B4.77 MiB1.5 ms
numpy.packbits位图625,000 B610 KiB0.87 ms
bitarray(N)625,000 B610 KiB0.17 ms
BHA:全 False 起点FalsesArray(N)415 B—0.12 ms
BHA:0.5% 解锁(25,000 个 True)100,132 B97.8 KiB1.58 s
BHA:5% 解锁(250,000)625,133 B610 KiB2.16 s
BHA:50% 解锁(250 万)625,135 B610 KiB2.30 s

几个一眼能看出的点:

  • 开局:FalsesArray(5_000_000)这个对象自身只有415 字节(memory_usage()口径),而 list 要 38 MiB;新成就上线那一瞬间,几百个成就同时铺出来也不占地方。注意 415B 只是容器自身记账,不含 import numpy 后解释器约 30 MiB 的常驻 RSS——比内存要看同一口径,本文横评统一用各容器的稳态memory_usage()。
  • 极稀疏(0.5%):同一 N、同一稳态口径下,BHA 用约 100 KB,是定长位图 610 KB 的约 1/6.2、numpy 布尔数组(np.zeros(N, dtype=bool),1 字节/元素)的约 1/50、list 的约 1/400。它内部这时候走的是array.array('I')升序存"已解锁下标"+ 二分查找,只记那 2.5 万个 True,而不是给 500 万人各开一个位。(顺带一提:numpy 布尔类型现在的规范写法是numpy.bool/dtype=bool,老写法np.bool_在 NumPy 2.x 里仍保留为np.bool的别名(2.0 起 canonical 名是np.bool/dtype=bool),并未被移除;我在本机 numpy 2.5.3 实测np.bool_ is np.bool为 True。)
  • 变稠密(实测 5% 已在位图态):0.5% 时它还在稀疏下标态;到 5%(25 万个 True)实测就已经位于按位打包的位图、625 KB——因为 25 万个下标光用 4 字节整型存就要约 1 MB,已经超过 N/8 的 625 KB,于是库在这之间自己切到了位图,到 50% 仍是 625 KB,和bitarray/packbits基本打平。表里 5%(625,133B)和 50%(625,135B)只差 2 字节,这点差异来自对象头部/计数等常数记账,与密度无关,理论交叉点约在 25 万下标×4B=1MB 反超 625KB 位图处(约 3.1%)。具体切换点以库当版行为为准,我这里只报告实测现象,不背固定阈值。

这也是这个库"hybrid"的字面意思:你不用自己手写"何时从下标表换位图"。

三步上手

安装(运行期硬依赖 numpy ≥ 1.19,不是零依赖,这点先讲清楚):

pipinstallbool-hybrid-array

游戏成就表最自然的用法,是从"全服未解锁"起步、再动态翻位:

frombool_hybrid_arrayimportFalsesArray N=5_000_000unlocked=FalsesArray(N)# 全 False,约 415Bunlocked[100042]=True# 玩家 100042 解锁unlocked[88]=Trueprint(unlocked[100042])# Trueprint(unlocked.count(True))# 2print(unlocked.memory_usage())# 稀疏阶段:约 100KB 量级(2.5 万 True 时 100,367B)

我实测从FalsesArray起逐个点亮 25,000 个分散成就,2.5 万次翻位合计约 0.0135 s(单次约 0.5 微秒),全程停在稀疏态、内存 100 KB 出头——这正是日常解锁事件在干的事。

如果你手里已经有一份解锁名单、要一次性建表,记得用生成器喂,别用列表推导(列表推导会先建一个满载的临时 list,把峰值内存抬上去):

frombool_hybrid_arrayimportBoolHybridArr rare=set(range(0,N,N//25000))unlocked=BoolHybridArr((iinrare)foriinrange(N))# 0.5% 稀疏

它的边界,我也一起摆桌上

不想把它写成银弹,三个真实代价你得知道:

  1. 从零一次性灌一个稠密大表并不快:上面 500 万元素用生成器逐个喂,BHA 要 1.5~2.3 s,而numpy.zeros靠 calloc 近乎 0。所以它的甜区是"稀疏起点 + 长期动态维护",不是"开机就把一个全满稠密数组灌进去"——那种活直接用 numpy。
  2. 稠密之后它就是一张位图:单点读取实测约 700~880 ns(timeit式对同一索引重复 200 次取中位,含 Python 下标包装,数值受 CPU 频率/缓存影响会有浮动,看数量级即可);纯定长位图 + 向量化批量统计场景,numpy/bitarray 更直接。
  3. 从源码层面看(9.13.11 安装包,PyPI 元数据只暴露裸numpy依赖,以下内部实现以源码为准):稠密侧省内存来自按位打包 N/8,位图缓冲由 numpy 的 uint8 承载(运行需要 numpy ≥ 1.19);稀疏侧是array.array('I')升序下标 + 二分,下标超过 2³² 才升到 8 字节的'Q'。顺带澄清一个常见误传:9.13.11 全程没有 RLE 行程编码,全 True/全 False 的极性塌缩也不是 RLE。

怎么选,一句话

  • 纯稠密、还要做向量化批量统计 → 老实用numpy;
  • 就要一张极简定长位图、逻辑越简单越好 →bitarray / bytearray / packbits;
  • 像成就、签到、标签、风控名单这种:稀疏度大起大落、要随时动态增删、还想要 list 一样的下标接口→ bool-hybrid-array 对味。

项目信息

  • 名称:bool-hybrid-array,PyPI 当前版本9.13.11;许可证Apache-2.0 OR MulanPSL-2.0(双许可,注意 MulanPSL 是 2.0),Python ≥ 3.8,硬依赖 numpy ≥ 1.19。
  • 主页:https://github.com/BKsell/bool-hybrid-array ;安装:pip install bool-hybrid-array
  • 核心类是BoolHybridArr(单数 Arr),另有TruesArray/FalsesArray快速极性构造。

面试那道题我最后没有背"哪个库最强",而是把这张表摆出来:成就解锁的曲线不是一条直线,存储也不该是。

别信我,信你自己机器上的测量。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进