十年

随着我敲下 git push origin master,轻轻按下回车,这一轮漫长的重构终于告一段落。

翻看提交记录时才发现,这些被重构的代码居然大多写于十年前,心里不免一阵唏嘘。

从 Git 仓库的第一个提交时间来看,silly 这个坑是我在 2015 年 6 月 27 日开的。当时的初衷,不过是想练习一下服务器编程。

可随着一次次思考与重构,我渐渐改变了初心。

我希望它能真正跑在服务器项目里,而不仅仅是个玩具。

大约在 2018 年,我第一次在一个卡牌游戏的塔防玩法中,将 silly 引入了生产环境。

这一点真的要感谢领导的开明。虽然只是一个子玩法,但引入新框架总是存在风险,尤其是数据错乱这类问题。所幸,silly 在上线后并没有带来额外的 bug。

不过,在内网倒是踩过坑。由于我们使用私有协议,需要自定义 Marshal/Unmarshal。为了提升效率,我当时加了 Cache 来减少内存分配,结果在链表尾部忘了置 NULL。这个Bug一直提醒我至今,每次写链表都会特别小心。

随着稳定运行时间的增加,我对 silly 的信心也逐年增长。

后来,在 SLG 游戏 的服务器程序中,我干脆用 silly 重写了整个业务逻辑。只有“武将养成”部分依然保留着 C 代码,因为那些代码是从之前的 卡牌游戏 继承下来的。

可以说,在这10年里,我真的学会了很多,这个框架也一直在和我一起成长。

但也正因为它一直在线上稳定运行,面对一些核心模块(比如网络模块)的设计时,哪怕心里有了新的想法,我始终下不定决心去动。

直到最近,我决定再次研究 io_uring

之所以说“再次”,是因为早在 2021 年我就粗略看过它的原理。当时查到的结论是:在网络场景里,io_uring 并没有显著优势,于是便搁置了。

几年过去,以它的发展趋势,我觉得值得再看看如今的性能究竟如何。

于是我开了一个分支,用 io_uring 重写了网络部分(虽然实现还有点小 bug,但不影响测试数据)。结果显示:性能仅略快于当前版本。

升级计划虽然再次搁置,但在重构过程中我却发现,现有的设计在一些地方已经显得颇为些掣肘。

再加上 2022 年工作上的变动,silly 继续用于生产环境的可能性也不大了。

我想,是时候为 silly 重新定位了。 它将作为我编程思考与最新领悟的承载体。当然,我依然会尽可能编写完善的测试来保证质量,只是在设计的选择上可能会更激进一些。

就在前几个月,我还跟别人说过:“我觉得自己终于算是入门了。”但要我清晰解释“入门”的含义,却又说不清。

所以,我决定对 silly 来一轮小规模的重构,来作为这句话的说明。

在写这篇文章时,我依然无法完整表达“入门”的含义,但似乎已经能尝试用一些场景来说明。


我刚毕业时,公司使用的 XML 文件巨大(上百 MB)。

即便是 tinyXML,在性能上也有些吃力。于是我从周六中午开始,直到晚上 11 点左右,写了一个简易的 XML 解析器。

具体性能我已记不清了,但至少比常规 XML 解析器快了 3~5 倍。至少在程序里选择芯片时,原本卡顿的界面不再那么难以忍受。

当然,世上没有银弹,我也没强到逆天。

真正的原因是:我根据公司 XML 文件的特点,结合内存分配和 CPU Cache 等机制,对代码做了高度定制化。

换句话说,换一类复杂点的 XML 文件,我的解析器可能就不行了,也未必快。

有个小插曲:我把设计思路讲给同事听,他也实现了一个,但性能总比我略慢。可惜当时只顾着沾沾自喜,没有仔细对比差异在哪里。

如果是十年后的我再遇到这个问题,我可能不会做出相同的选择。

我可能会先考虑:到底是什么导致了卡顿?是 CPU 算力不足,还是单核性能不够,还是文件过大、IO 过慢?

假如确认是单核性能不足,我可能会考虑在打包安装包时,提前把 XML 拆成多个文件,运行时多线程加载,再合并数据。

如果多线程依然慢,我会结合表现层需求(界面是下拉框 + 芯片 ListBox,当切换厂商时显示对应芯片)去做二级索引:

  • 每个厂商的芯片数据预处理成独立 XML;
  • 再生成一张厂商表;
  • 打开界面时,只加载厂商列表和默认厂商的芯片数据;
  • 切换厂商时,按需加载对应文件;
  • 甚至后台异步加载所有文件。

这样,流畅度提升的数量级,绝对比单纯重写解析器要大得多。

在我之后的职业生涯中,这种“通过裁剪功能或换思路来提升性能”的场景一次次重演。


再说回这次的重构。跨线程通信时,我需要用到队列。

巧的是,当年写 XML 解析器的那个程序,也用了多线程。一个上位机要同时和 64 块烧录板通信。

为了简单,我给每个烧录板开了一个线程。正常烧录一切顺利,直到电饭煲厂家提了个新需求:每个设备都要烧录一个唯一的 device id,而且 id 由他们提供。于是我写了个 SPMC 队列来解决 64 个线程的竞争问题。

现在回头看,当时是否真的有必要,仍是个问号。我甚至没去验证这个队列相比加锁快多少,或者说“快这么多”到底有没有意义。

这次我特意问了 AI,它强烈推荐我用 SPSC 队列,因为通信只有两个线程。

但仔细思考后,我还是拒绝了。最终实现了一个简单的 PING-PONG 双缓冲机制来替代重锁。

相比 SPSC 队列的复杂性,在性能牺牲不大的情况下,我更倾向于选择更直观的设计。


有时候,我们看到别人的代码非常简单,我们可能会说,这么简单的代码谁不会写。

可当自己面对相同问题时,能否也选择如此简单的方案,这才是对我们的考验。

正如乔峰聚贤庄一战,太祖长拳照样胜过群雄——这便是“化腐朽为神奇”的能力。

我想说的“入门”大抵就是如此了, 能用最常规的数据结构和实现来解决问题,而不是像过去那样,总想着在局部动刀裁剪来换性能,而忽视了从全局出发去思考。

当然,还有一些难以言说的感受。

PS:既然我说“入门”,就意味着我还远未精通,只是刚好触到这一层认知而已。

发表评论

nine × = 18