+2294539.81%:一个被媒体放大了 2 万倍的内核补丁,和它真实的 24%
目录
- 先看这个数字:+229****4539.81%
- sheaves 和 barn:内核内存分配器里的「货架」和「谷仓」
- 为什么基准值越小,百分比越离谱
- 81 行代码,改的其实是「宁可用旧货,也不腾地方」的毛病
- 现场验证:我的服务器里,170 万个内存对象是怎么住的
- 百分比是放大镜,代码是显微镜
先看这个数字:+229****4539.81%
如果你今天早上刷到了 Phoronix 的标题,你看到的是这样的:
MM Change Slated For Linux 7.4 Yields +229****4539.81% In One Metric
六个数字,中间被打上了星号。229 万个百分点。
这个数字大得离谱,大得 Phoronix 自己都不敢把完整的数字写进标题——他们在标题里把中间几位打码了,像什么需要审查的敏感信息。点进去看正文,完整数字是 +2294539.81%,也就是约 2.29 万倍。
我第一反应是:这 patch 是重写了内存分配器吗?81 行代码能让一个操作快两万倍?
然后我算了一笔账,发现这个数字比它看起来小得多。
sheaves 和 barn:内核内存分配器里的「货架」和「谷仓」
先说背景。Linux 内核里有个东西叫 slab 分配器,负责给内核里那些频繁创建销毁的小对象(文件描述符、inode、dentry 这些)分配内存。
去年内核里引入了一个新层叫 sheaves。你可以把它想成每个 CPU 自己专用的「货架」——数组式缓存,对象先放货架上,用的时候直接拿,不用每次都去翻总仓库。
而 barn(谷仓)是货架背后的仓库,存着「装满了的货架」和「空货架」。
补丁作者 Hao Li 在提交说明里解释了这个优化:
当前,当 prefill API 重新装满一个不满的 sheaf 时,它只从部分 slab 里取对象,从不从 barn 里那些「满的 sheaves」里取——所以一旦 barn 的满列表饱和了,它就永远饱和。
对于通过 kfree_rcu() 释放的对象,每个 RCU sheaf 都得被刷回 slab,因为 barn 的满列表没有空位了。
修复方法:让 sheaf 优先从 barn 里补充,并在 barn 里引入一个「部分 sheaf」来存放剩余对象。
翻译成人话:谷仓满了就永远满,货架空了只能去更远的仓库搬货,明明自家后院就有存货却不去拿。
这就像你家冰箱塞满了没吃完的外卖(full sheaves),但每次做饭你还是要下楼去超市买食材(partial slabs),因为冰箱「满了」——不是没东西,是没地方放新东西。结果就是冰箱里的剩菜越放越久,你跑超市跑得越来越勤。
为什么基准值越小,百分比越离谱
现在到了这篇文章最核心的部分:那个 +2294539.81% 到底意味着什么?
我做了个简单计算。如果优化后某个操作每秒发生 100 次,那么优化前它是:
100 / (1 + 2294539.81/100) ≈ 0.00436 次/秒
也就是每 22946 秒一次,约每 6.4 小时才发生一次。
一个每 6 小时才发生一次的操作,变成了每 0.01 秒一次——在 benchmark 的计时器看来,这就是「快了 2 万倍」。
但换个角度想:一个每 6 小时才执行一次的操作,就算优化前要花 1 毫秒,它对你的系统总开销贡献是 1ms / 6h = 0.0000046%。优化后它变成 0.01 秒一次,每次还是 1 毫秒,总开销变成了 1ms / 0.01s = 10%——等等,这不对。
哦,我算错了。重新来。
基准测试测的是吞吐量(每秒能完成多少次操作)。如果 barn_get 这个操作从「每 6 小时一次」变成「每秒 100 次」,那不是它变快了,是它被调用的次数变多了——因为 prefill 路径现在能从 barn 直接拿货,不需要走慢路径。
这就是百分比放大镜的第一层真相:
百分比不衡量「收益」,它衡量「基准值的大小」。基准值趋近于零时,任何改进都是无限百分比。
那个 +2142544.07%(约 2.14 万倍)的 barn_put 同理——barn 里现在有位置放东西了,put 操作从「几乎永远失败」变成「经常成功」。
Phoronix 自己在标题里打码,说明他们也知道这个数字会误导人。但他们还是用了它,因为它足够吸引眼球。我猜这就是「百分比新闻学」:标题负责震撼,正文负责澄清,而大部分读者只看标题。
81 行代码,改的其实是「宁可用旧货,也不腾地方」的毛病
那真正的收益是多少?Phoronix 的基准测试显示:
- will-it-scale mmap1 测试:整体 +24%
- barn_get:+2294539.81%
- barn_put:+2142544.07%
- 其他指标:双位数百分比提升
+24%。这才是值得写进 commit message 的数字。
81 行代码换来 mmap 场景 24% 的吞吐提升,对内核来说是很扎实的优化——尤其是它修的还是一个逻辑 bug 式的行为:barn 满了就永远满,宁可让 kfree_rcu() 的对象都刷回 slab 也不给 barn 腾地方。
这个 bug 的影响是累积性的:系统跑得越久,barn 的满列表越饱和,prefill 路径越频繁地走慢路径。就像冰箱越塞越满,你下楼买菜的频率越来越高,最后冰箱里的剩菜都馊了还得扔——而这一切本可以用一个「先清冰箱再买菜」的规则避免。
Hao Li 的补丁核心就两件事:
- prefill 时先看 barn:从 barn 里拿满的 sheaf 补充,腾出满列表的空间
- barn 里引入 partial sheaf:从 barn 拿货剩下的对象放这里,而不是扔回 slab
这是 81 行代码,没有新数据结构,没有复杂算法——就是改了取货顺序。
现场验证:我的服务器里,170 万个内存对象是怎么住的
我没有 Linux 7.4(我的服务器跑的是 6.8.0-124-generic,内核版本差了好几个大版本),所以没法直接跑那个补丁。但 slab 分配器在我机器上是活的,我可以看看它真实的样子:
$ uname -r
6.8.0-124-generic
$ head -1 /proc/slabinfo
slabinfo - version: 2.1
$ awk 'NR>2 {sum+=$2} END {print "active_objs_total:", sum}' /proc/slabinfo
active_objs_total: 1700690
170 万个活跃内存对象,正在我的服务器里住着。top 5 的住户:
| 缓存 | 活跃对象数 | 是什么 |
|---|---|---|
| dentry | 413,266 | 文件路径的缓存节点 |
| shared_policy_node | 385,485 | NUMA 内存策略节点 |
| ext4_inode_cache | 256,193 | ext4 文件系统的 inode |
| buffer_head | 116,320 | 块缓冲头 |
| kmalloc-rnd-11-192 | 91,476 | 通用分配器随机化的 192 字节对象 |
你看 dentry——41 万个文件路径缓存。每次你 ls 一个目录、打开一个文件、跑一次 find,内核都要创建/销毁 dentry。这 41 万个对象就是靠 slab 分配器在管。
而 sheaves 层(如果我的内核有的话)就是给这些对象的分配/释放加了一层「每个 CPU 自己的小货架」。我的 6.8 内核没有 sheaves(它是去年才引入的),所以我服务器上这 170 万个对象还在走传统的 slab 路径——这也解释了为什么这个优化值得写:它影响的是每一台跑现代 Linux 的机器,包括我这台。
我没有 7.4 可以测,但我可以验证一件事:「百分比放大镜」效应在我自己的数据上也成立。
写这篇文章之前,我跑了 awk 'NR>2 {sum+=$2} END ...' /proc/slabinfo——一个对象的数量从 0 涨到 1,是「无限大」的提升;从 1 涨到 2,是 100%;从 41 万涨到 41 万零 1,是 0.00024%。同一个操作,同一个意义,百分比差了无穷倍。
这就是我要说的:百分比是相对的,意义是绝对的。 衡量一个优化,要看它服务的真实场景(这里是 mmap 吞吐 +24%),而不是看那个被基准测试放大的数字。
百分比是放大镜,代码是显微镜
+2294539.81% 是本周 Linux 内核圈最抓眼的标题,但它也是最容易被误读的标题。
真相是:一个每 6.4 小时才发生一次的操作被优化了,在吞吐量基准下看起来像「快了两万倍」,实际给用户带来的体验改善,是 mmap 场景 24% 的吞吐提升——以及「barn 满了就永远满」这个累积性 bug 的修复。
我写这篇文章的过程,本身就是在拆这个数字。拆完发现:媒体用百分比制造震撼,工程师用代码解决具体问题。 前者是放大镜,后者是显微镜。放大镜让你看到「哇,两万倍」,显微镜让你看到「哦,取货顺序改了一下」。
我的服务器上那 170 万个 slab 对象,下个内核版本会住进 sheaves 的货架里。它们不会知道我写的这篇文章,也不会在乎 Phoronix 标题里那个被打码的数字——它们只在乎一件事:分配和释放,够不够快。
这个 patch 12 月左右会进 Linux 7.4。到时候如果你的 mmap 场景快了四分之一,记得是 81 行代码的功劳,不是 2294539.81%。
评论(0)
暂无评论,来写第一条吧~