ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

我踩的那些 Python 布尔数组的坑

我踩的那些 Python 布尔数组的坑 那天我在做日志埋点数据的去重统计每个用户当天有没有触发过某个事件有就是 True没有就是 False。数据量不大大概几百万行日志我觉得随便写写就行了。上线那天下午运维突然打电话说机器内存炸了。我说不可能啊我代码写得挺简单的啊。运维在电话那头沉默了两秒然后幽幽地来了一句你自己看吧那语气我到现在都记得像极了班主任发现你作业是抄的。我当时写的就是个普普通通的 list装了一堆 True 和 False。打开终端一看进程被 kill 了理由是 OOM。flags[False]*50_000_000# 一个 True/False 在 Python 里是完整对象约 28 字节# 五千万个就是 1.4GB还没开始算就 OOM 了我这才意识到一个 True 在 Python 里可不是一个 bit它是个完整的 Python 对象背后有一整套引用计数和类型信息这些东西一个布尔值吃掉了差不多 28 个字节。几千万个 28 字节堆在一起直接就翻车了。你说气不气我明明只存了个是和否Python 非要给我配一整套户口本。那一刻我脑子里就一句话这波血亏。我甚至怀疑 Python 是不是觉得我存的是个有身份的布尔值得给它上户口、办社保、交公积金。后来我换成了 array以为 stdlib 的东西总归靠谱。写的时候还挺高兴内存确实降下来了几千万条记录也能跑。我当时还跟同事吹牛说你看标准库就是稳一个字节一个布尔这不就解决了嘛。fromarrayimportarray flagsarray(b,[0])*50_000_000# 每个元素只占 1 字节内存确实下来了# 但业务线一扩长度一变它又开始吃紧结果改需求了老板说要扩充到好几个业务线数据量大了一截array 又倒了。我心里窝火得很感觉每个方案都只差那么一口气就像打游戏差最后一刀就能通关结果 boss 狂暴了。这波啊这波是我裂开了。我那天晚上回家路上还在想是不是我写代码的姿势不对要不要去庙里拜拜。然后我想到 numpy。写数据处理嘛迟早得用它的。用了半年没事直到有一次上游来了一个亿的数据直接把进程干掉了。numpy 的 bool_ 虽然是比 list 省多了但一个 bool 也要一个字节一个亿就是一百兆左右再加上副本、临时变量、广播计算实际跑起来内存经常爆表。importnumpyasnp flagsnp.zeros(100_000_000,dtypebool)# bool_ 一个占 1 字节一亿就是 100MB# 加上副本和广播内存直接爆表那种报错框跳出来的时候我就坐在椅子上发呆心想一个亿就炸了后面还有十亿的量等着呢。这感觉就像你刚换了个大点的钱包结果工资还没捂热账单先来了。我当时只想对 numpy 说一句你礼貌吗我甚至能脑补出 numpy 的内心独白你要的是省内存我可没说我不吃内存啊。我这时候有点急了开始翻各种文档。找到 bitarray 的时候觉得看到了光一个 bit 存一个布尔值这才是正解啊。一亿个布尔值才十几兆内存瞬间就安全了。我当时激动得差点在工位上喊出来同事以为我中了彩票。frombitarrayimportbitarray flagsbitarray(1_000_000_000)# 一个 bit 存一个布尔值内存确实省# 但十亿个 bit 搬进 CPU 的那条路成了新的瓶颈我高高兴兴换成 bitarray 跑了两天然后数据量又涨了过了十亿之后它也开始扛不住。不是内存不够而是操作变得很慢。我后来才明白十几兆看着小但数据量大了以后从内存往 CPU 搬数据这件事本身就变成了瓶颈内存占用多就会卡这就是所谓的内存墙普通人直观感受就是电脑风扇狂转、进度条凝固罢了。就像你搬家东西是少了但路就那么宽一趟趟搬反而更慢。这大概就是传说中的路虽近堵车。我那时候盯着风扇狂转的电脑感觉它不是在跑程序是在跟我抗议。也不是没人推荐我用 scipy.sparse。我试了文档写得是真晦涩sparse.csr_matrix 把布尔数据转成稀疏矩阵处理。写起来还行查那个 nonzero 的速度也挺利索。但在我的场景里密度不固定有时候稀疏部分突然变密它就僵在那儿转圈了。fromscipy.sparseimportcsr_matrix flagscsr_matrix((1,100_000_000),dtypebool)# 稀疏矩阵适合数学场景密度一上来就僵住# 函数调用开销大千万次叠加就卡sparse 的初衷是处理数学上的稀疏矩阵跟我的布尔序列批量标记匹配不是一回事函数调一次开销大千万次叠加起来就变成肉眼可见的卡顿。这就像你拿了个专业单反去拍大头贴功能是强大但快门按下去那一下够你喝杯茶了。我只能说这波是杀鸡用牛刀牛刀还卡壳。我甚至怀疑 scipy 是不是觉得我这种用法是在侮辱它。把 scipy.sparse 删掉那天下午我随手在 GitHub 上瞎搜纯粹是碰运气。翻了好几个不相关的项目最后撞上一个叫 bool-hybrid-array 的库星星少得可怜但因为名字里有 hybrid 我就点进去了。我当时想的是反正都这样了看看也不亏。说实话第一眼看到那个 star 数我心里是打鼓的毕竟谁都不想在生产环境里赌一个随时可能没人维护的库。但翻完 README 和源码我反而踏实了它底层就是 numpy 和 array 这两个我天天在用的东西没有自己造轮子没有黑魔法核心逻辑就几百行我甚至能一行行看懂它在干嘛。这种库反而让我放心因为它不依赖什么玄学就是老老实实把两个成熟方案拼在一起出问题我也能自己进去改。看了两眼它的逻辑我就傻了。它居然把数组切成两块一块是密集区用 numpy.ndarray 存因为多数是连续 True 或连续 False这部分操作快另一块是稀疏区用 array.array 存因为是零散的异常值只记录索引就够了。它不是猜它是自动根据内容判断该走哪条路。我拿自己那个快把我逼疯的数据塞进去跑内存从过去的百兆级别直接掉到了十几兆手指点在回车上的瞬间已经返回结果了。那一刻我差点从椅子上跳起来感觉像在垃圾堆里捡到了宝。这波啊这波是真香。我甚至想给这个库的作者发个红包虽然我不认识他。后来我又仔细看了下它的依赖就 numpy 和 array都是 Python 生态里最老牌、最稳的东西它自己只是薄薄一层封装没有引入任何冷门依赖。这意味着就算这个库哪天不更新了只要 numpy 和 Python 还在它就能一直跑我甚至可以直接把它的核心逻辑抄进自己项目里反正就几百行。这种底层全是熟脸的库用起来是真的安心。我后来专门试了一下修改长度的情况。很多方案一开始内存小跑得快但只要长度一改比如追加或删除一段内存立刻就掀翻。list 的扩容策略会预留空间array 也一样numpy 更狠改长度意味着整块重新申请内存一亿规模的数组 resize 一下程序直接僵住。我试过在 numpy 上动态追加数据每次 append 都像在开盲盒不知道哪次就内存错误了。这个方案的好处是它两面都有密集部分用 numpy但那部分长度是不怎么变的稀疏部分用 array长度可变但没有重新申请整块的压力。长度变化时的内存墙在它这儿不算是绝症因为它把变量部分放在了负担最轻的地方。我后来想想这就像搬家的时候把重的东西放固定位置轻的东西随便挪反而最省力。而且它没有搞什么花里胡哨的 C 扩展或者编译步骤pip 装完就能用跟装 numpy 一样省心不会出现那种装个库还要配半天编译环境的糟心事。对我来说一个库稳不稳就看两件事一是底层是不是熟脸二是装起来麻不麻烦这两点它都过关了。我现在回想起那些半夜对着 top 命令发呆的日子就觉得好笑。最开始只是一个 list后来 array、numpy、bitarray、scipy.sparse一个接一个地试每次都是换个场景换个死法。直到翻到一个没几个人 star 的库才把这条路走通。有时候真觉得踩坑这事儿踩得多了坑都认识你了。这大概就是传说中的菜是原罪但菜着菜着也就熟了。我现在看到内存相关的报错第一反应不是慌而是先笑一声然后默默打开 GitHub 开始搜。不过说真的我一开始也担心过这个库会不会哪天就没人管了。毕竟 star 数摆在那儿谁看了都得犹豫一下。后来我实在憋不住直接去 GitHub 上给作者发了封邮件问他这库还维护不维护。结果他回得挺快说只要他没事就不会停止维护有时候忙起来可能几个月没动静但一闲下来就疯狂更新跟打了鸡血似的。我顺手翻了翻版本历史好家伙真不是吹的隔三差五就一个 commit有时候一天好几个那更新频率比我写日报还勤。看到这儿我彻底放心了这哪是没人管的弃坑项目分明是作者拿它当亲儿子在养。而且它底层就依赖 numpy 和 array 这两个 Python 生态里最老牌、最稳的库没有引入任何冷门依赖没有自己造轮子没有黑魔法。这意味着就算哪天它真不更新了只要 numpy 和 Python 还在它就能一直跑。我甚至可以直接把它的核心逻辑抄进自己项目里反正代码就摆在那儿出问题我也能自己进去改。这种底层全是熟脸的库用起来是真的安心比那些动不动就拉一堆依赖、装个库还要配半天编译环境的家伙靠谱多了。
返回列表