先说个我至今没搞明白的事:有人真的花了力气,去测 PHP 里一百万个整数占多少内存。不是"大概几十兆吧",是精确到字节——16,781,392。
配表格、配进程对比、配扩展编译过程,一整篇长文。
我看到的第一反应是:这人是不是被什么逼的。
第二反应是:我被什么逼的,为什么我能一口气读完。
顺序塞和倒着塞,差 2.5 倍
同样一百万个整数,同样一百万个键。从 0 顺着往后塞,一个 16.78 字节;从 N-1 倒着往回塞,一个 41.94 字节。
差 2.5 倍。
packed 数组不存键这件事,同行心里都有数,我知道。但这数字值得单拎出来看一眼,因为它解释了一个特别经典的加班现场:
你从库里捞出十万条记录,按 id 排好塞进数组,跑得好好的。后来需求说"我要最新的在前面",你顺手加个 array_reverse,或者干脆从后往前循环塞。功能没变,数据没变,内存翻了两倍半。
然后你盯着监控图找了一下午,怀疑缓存泄漏、怀疑 ORM 生成重复对象、怀疑 opcache 没热——最后发现问题出在你把数组倒过来写了。
(友情提示:这不是玄学,这是哈希表在问你"你到底要不要按顺序来"。)
一个字符串键,25 MB
一百万个 packed 整数,16.78 MB。往里塞一个字符串键,涨 25,161,728 字节。
一个键。25 MB。
而且你把那个键 unset 掉,这 25 MB 一分不还。
我当场就不困了。这放在生活里叫什么——你为了一个可能永远不来的东西,先把保证金交了,退不回来。防御性设计就是这么长出来的:可能性从没发生,代价留在账上。
更离谱的是往百万数组里加键 -1,同样是 25 MB。就因为是负键,整个结构当场转哈希。
【灯光渐暗,程序员盯着内存曲线,脸上是那种"我认了"的表情】
所以那篇文章里那句提醒很到位:别让字符串键混进本该是列表的数组。听着像废话——踩过的人才知道这不是废话,是拿凌晨三点换来的。
删了 999,000 个元素,内存变化 0 字节
从一百万元素里删掉 999,000 个,memory_get_usage() 变化 0 字节。
一个都不还。
表会记住它曾经达到的最大尺寸,然后再也不提。我不是在说数组,我在说某个阶段的我。
这条老 PHP 人多少听过,但每次听到还是会心梗,因为它跟"我都删了那么多,数组当然缩回去了"这个直觉差太远。你自己写个循环删一半,看内存曲线纹丝不动,第一反应是查询写错了。
评论区有人说 array_values() 或者 sort() 能强制压回 packed 布局,但表本身不会自己缩。还有人说这是自己继续留在 8.1 的理由——我看到这句笑了,为了一个 unset 之后的行为留在一个旧版本,这理由真的很程序员。
重点压根不在百万级
评论区有人问:为什么非要一百万行,谁会逆序填索引,正常写代码根本碰不上。
作者回了一句我挺喜欢的:百万规模只是为了让每个元素的数字好读,比例不依赖规模。
对,这才是我最想说的。那篇东西表面上是内存测量,实际是一份"你以为你在写列表,其实你在写哈希表"的现场勘查报告。而且触发场景全都很日常:
array_filter() 之后结果保留键。你过滤出一百万个偶数,元素少了一半,内存几乎没少——它在第五个元素那儿就撞上条件转哈希了,然后按哈希表的排面占着。
对 filter 的结果调一次 array_values(),直接从 41.94 字节/元素掉到 16.79。同一个数据,同一个数量,两倍多。
array_is_list() 看的是键,不是布局。它可能对一个哈希表返回 true——它只认键长得像列表,不认内存在哭。
我上次那个说需求方朝令夕改的段子,其实和这个是一回事:改来改去,改的只是内容,容器没动过。需求文档从第一版改到第七版,评审会上那句"这次真的不会再改了"——幂等。改到最后还是那一大坨,只是里面每个槽都退化成哈希了。
有个细节我盯着看了很久
两个数组,都两个元素,内容一样,就是插入顺序不同:[0=>'b', 1=>'a'] 占 216 字节,[1=>'a', 0=>'b'] 占 376 字节。
多 74%。
因为后者从键 1 开始,初始表装不下,直接建了 8 个桶 16 个索引槽的哈希表。你说这上哪说理去。
这个数字比百万级那个更值得发给同事看——够小、够具体、一杯咖啡的工夫就能复现,又能让人当场陷入沉默。
(郑重声明:以上数字我都没亲自跑,但我信,因为我以前被同一类问题折磨过。数字不重要,被折磨过这个事实重要。)
数组还是对象,这事以前我们比错了
那篇文章最后给的建议都很朴素:保持列表 packed、按顺序追加、别让字符串键混进列表、filter 之后长期存活的调一下 array_values()。它还提了一句我特别认同——要常驻内存的行,用小 final 类加构造器提升,内存大约是关联数组的一半,readonly 不额外花钱。
平时我们讨论"用数组还是用对象",比的是可读性和 IDE 提示,很少有人真去比内存。数据摆在这:小 final 类 233 字节,stdClass 529,关联数组 481,packed list 321。
stdClass 和关联数组几乎一样贵——因为它本质上就是"一个哈希数组外面套了个对象头"。你以为你在用对象,其实你在用一个穿了西装的关联数组。
还有生成器那段。先把所有行物化再遍历,峰值涨 241 MB;流式 yield,涨 37 KB。三个数量级。
我看到这个数字的第一反应是"这不废话吗",第二反应是"我这周就写过先物化的版本"。
所以写段子最难的地方从来不是没素材,是素材读完,你发现笑的那个人是你自己。
(友情提示:本文中关于本人的技术行为均为艺术加工,如有雷同,说明我们大概在同一家公司。评论区扣个"我的数组也不还内存",我们下条再见 🫠)
