昨天我写了一篇文章,主要是在说:当我真的拿Zig去写一个KV数据库之后,我开始理解这门语言到底擅长什么。但说实话,昨天那篇文章还比较偏“感受”。昨晚,我在kvdb的develop分支上狠狠干了一轮优化,做到后面突然有种更具体的感觉:数据库优化这件事,很多时候根本不是你发明了什
昨天我写了一篇文章,主要是在说:当我真的拿 Zig 去写一个 KV 数据库之后,我开始理解这门语言到底擅长什么。
但说实话,昨天那篇文章还比较偏“感受”。
昨晚,我在 kvdb 的 develop 分支上狠狠干了一轮优化,做到后面突然有种更具体的感觉:
数据库优化这件事,很多时候根本不是你发明了什么惊天算法,而是你开始认真对待每一次内存分配、每一次磁盘调用、每一次多余的树遍历。
也就是说,优化不是“变魔术”,而是“别浪费”。
很多人一提性能优化,脑子里马上就会冒出一堆高大上的词:
锁优化、零拷贝、SIMD、缓存友好、分层索引、批处理、并发调度……
这些当然都重要。
但我昨晚最大的体感反而是:
真正先把程序拖慢的,往往不是缺少某个高级技巧,而是代码里有太多“理所当然但其实很贵”的动作。
比如:
先查 key 在不在,再决定插入还是更新; 每次更新都把整页重新整理一遍; 内部节点插入时,先收集再重建; 写一条 WAL,拆成很多小段慢慢写; 读一个页,先 seek 再 read。
这些写法都“没错”,而且刚写出来的时候通常还觉得挺自然。
但数据库一旦开始跑起来,操作次数上来之后,这些“自然的动作”就会变成真实的成本。
这轮优化里,最核心的事情不是加功能,而是删浪费。
我越来越觉得,数据库内核开发很像收拾一个本来能住、但动线很差的房子。
不是说房子不能住。 而是你走两步就撞一下,转身又多绕一下,拿个杯子要多开一次柜门。 单次看起来都不大,但一天重复一百次,人就烦了。
程序也是一样。
WAL 这个东西,本质上就是“正式改数据之前,先把变更记到日志里”。
它的意义谁都知道:防崩溃,做恢复。
但真正影响性能的,不只是“有没有 WAL”,而是“你怎么写 WAL”。
如果一条日志要先在堆上拼很多临时块,再分很多次写出去,那每次写入都在给自己加戏。昨晚我做的一个重点,就是尽量把记录在栈上组织好,再更整块地写出去。
这类优化的本质很朴素:
不是别写日志,而是别把一条本来很短的路,硬走成绕远路。
以前很容易写出这种逻辑:
先 contains 看 key 在不在;
不在就插入;
在就更新。
看着特别合理。
问题是,数据库里的“看一眼”可不是白看的。 每次判断存在性,本质上都要再走一遍 B-tree。
所以后来我把思路改成了:
先直接写,只有真正撞上“key 已存在”时,再切更新分支。
这事给我的感受特别强烈:
有时候你以为自己在“谨慎”,实际上你是在让程序白干活。
这个优化我自己很喜欢。
数据库页内部的数据布局,本来就很珍贵。你想想,页里已经放好了那么多 key/value,位置都排好了,这本身就是一种“局部秩序”。
如果一次更新只是把旧值换成更短的新值,而且原来的位置明明还放得下,那最合理的做法其实就是原地改。
但如果你不管三七二十一,每次都把整页重新整理一遍,那其实是在为了省脑子,牺牲机器。
这个优化带给我的感觉是:
很多时候,真正好的优化,不是更激进地折腾数据,而是更克制地少碰数据。
B-tree 不只是叶子节点会动,内部节点也会变。
以前内部节点插入时,一个很顺手的做法是:先把内容收集出来,插进去,再整份重建。
逻辑是简单了。 但代价也很明显:分配新内存、拷贝、回写。
后来改成原地 memmove 之后,那种感觉特别像:
以前是为了在名单中间插一个名字,先把整张名单抄一遍; 现在是把后面的人往后挪一个位置,把新名字塞进去。
代码未必更“优雅”,但会更诚实。
它不是搭新房子。 它更像大扫除。
你不会因为把房间打扫干净,就发明了一个新宇宙。 你只是终于把那些堆在角落里的破纸箱、塑料袋、空瓶子、废电线,一点点清走了。
但一旦清完,你会发现整个空间 suddenly 就通了。
程序优化也是这种感觉。
不是每次都要上复杂理论。 很多时候,你只是把这些东西清掉:
没必要的分配, 没必要的拷贝, 没必要的 syscall, 没必要的树遍历, 没必要的页重排。
然后系统自然就顺了。
昨天我还在说 Zig 给我的感觉是“更贴近底层”。
但昨晚狠狠干这一轮后,我觉得更准确的说法应该是:
Zig 会逼你不断意识到成本。
这一步到底有没有分配? 这个 buffer 能不能放栈上? 这里为什么要复制? 这里真的要重排整页吗? 这个抽象是在帮我,还是在偷偷花钱?
这种感觉在写数据库时尤其强。
因为数据库是非常不讲情面的。 你代码里只要多做了一点点没必要的事,最后都会被吞吐、延迟、磁盘写放大老老实实地揭穿。
写到现在,我最大的感受不是“我做出了一个多厉害的数据库”。
而是:
我终于开始理解,数据库为什么是系统编程里最诚实的一类东西。
你多分配一次,它知道。 你多拷贝一次,它知道。 你多走一遍树,它知道。 你多发一个 syscall,它也知道。
它不会因为你代码写得很漂亮,就假装没看到这些成本。
所以昨晚这轮优化,虽然从外面看没有什么特别炸裂的新功能,但它让我第一次特别具体地明白:
所谓性能优化,很多时候就是把那些“本来以为理所当然”的浪费,一个个揪出来。
而这,可能才是从“做了个数据库玩具”,走向“开始理解数据库内核”的真正开始。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!