我的 JavaScript 比你的 Rust 更快
Josh Urbane 是一位从业多年的软件架构师,很喜欢在社交媒体分享技术观点。近日,他写了一篇文章,记录了自己凭借经验赢了与新人开发者打赌的故事,而“我的 JavaScript 比你的 Rust 更快”的结论也是来自这个打赌。他的故事或许可以说明运行策略在研发实践中的重要性。
对我来说,软件架构师这活儿最让人开心的一点就是能指导开发者理解最新的概念、影响他们的技术判断。有些开发者不是很嚣张吗,那就用理论加现实啪啪打他们的脸;架构师还得负责营造出寓教于乐的学习氛围,帮助年轻气盛的开发者逐渐长大成熟。
最会让我在心里暗爽的事儿就是一个愣头青开发者突然跳出来、想要挑战我的技术建议(从开发者的视角看,架构师就是一帮总在提「错误」建议的傻瓜),而且赌上全部身家坚持认为自己的办法更好。
问题是,我已经干这行很久了,不用验证我就知道问题的正确答案是什么。所以那就来呗,咱们手底下见真章,我把这段故事记录了下来、在几年后整理成了今天的这篇文章。
梭哈是一种“智慧”
老实讲,下面要讲的这个事已经过去好几年了,所以很多细节我已经记不清楚。大体情况就是结合当时团队的知识储备、可用工具库和原有技术债务,我给出的建议是让大家使用 Node.js。
一个新任初级开发者对自己刚拿到的计算机科学学士证书很有信心,想要用“炫技”的方式挫挫我的锐气。他们听说我是辅修的计算机科学,所以觉得我压根不了解计算机底层原理。其实刚毕业那会我也认为自己很懂,但这行干久了,我越来越觉得计算机系统像是魔法……
他的信心并非毫无来由,这个结论如同“C++ 比 JavaScript 速度快”,基本属于业界共识。但作为典型的架构师,我仍然坚持认为“要视情况而定”。
更具体地讲,“经过充分优化的 C++,确实比具有同等优化水平的 JavaScript 跑得更快”,毕竟 JavaScript 有着无法避免的执行开销(即便如此,我们也可以把代码编译成静态程序来获得高度接近 C++ 的性能)。反正话已至此,那就梭了呗。
意外的是,JavaScript 代码确实要比 C++ 版本更快一点,而且从架构设计的角度来看,JS 版本可以由当前团队一力维护、不需要借助其他部门的技术能力。
还好还好,其实我也不敢百分之百确定自己是对的,但考虑到这个用例中的内存对象大小可能是动态的、再加上那位年轻开发者确实经验不足,所以我愿意赌上一把。
JS 比 C++ 还快,怎么实现的?
我猜大多数开发者都理解不了这样的结果。这明显跟“编译”语言快于“解释”语言、“静态”程序快于“VM”程序的基本原则背道而驰啊。但请注意,这些只是经验、而非真理。
我之前也提到,“优化”才是决定速度的关键。毕竟即使 C++ 语言自身的性能优势再强,糟糕的编写质量也会让程序身陷泥潭。另一方面,Node.js(使用基于 C++/C 的 V8 与 libuv 库)则更具优化空间,所以实际运行速度并不差。甚至可以说,质量同样差劲的 JS 和 C++ 程序,JS 的性能可能还更好一点。但这只是宏观论述,下面咱们来看点细节。
内存是关键
大多数开发者应该很熟悉栈和堆的概念,但这种理解基本只停留在了表面——例如只知道栈是线性的,而堆就是带有指针的“坨”(并非严格术语,大家能理解就行)。
更重要的是,栈和堆的概念对应着多种实现和方法。底层硬件并不知道“堆”是个什么东西,因为内存的管理方式是由软件来定义的,而内存管理方面的选择必然会对程序的最终性能产生巨大影响。
大家也可以就这个问题深挖下去,很有意义也很有价值。现代硬件和内核都相当复杂,其中往往包含大量具有特殊用途的优化机制,例如更高效地利用高级内存布局。这意味着软件可以(或者必须)借用由硬件提供的内存管理功能。此外还有虚拟化的影响……这里就不多做展开了。
魔法的核心:垃圾回收
没错,Node.js 解决方案的启动时间肯定更长,因为它需要通过 JIT 编译器来实现脚本的加载和运行。不过一旦加载完成,Node.js 代码其实反而拥有一项神秘的优势——垃圾回收机制。
而在 C++ 程序中,应用程序往往会在堆中创建动态大小的对象,之后再将其删除。这意味着程序的分配器必须一遍又一遍地在堆中分配和释放内存。这项操作本身速度较慢,而且实际性能基本由分配器中的算法决定。在多数情况下,dealloc 的速度会特别慢,即使是精简后的 alloc 也没好太多。
对于 Node.js 程序,这项绝技就是程序只运行一次就会退出。Node.js 同样运行脚本并分配必要的内存,但后面的删除操作会由垃圾回收器挑选空闲时间再推迟执行。
诚然,垃圾回收机制在本质上并不比其他内存管理策略更好或者更差(一切都是权衡),但在我们打赌的这个特定程序中,垃圾回收确实能显著提升性能,因为这个程序压根就没真正运行过。我们只是把一大堆对象塞进内存,再在退出时一次性丢弃。
垃圾回收肯定是有代价的,Node.js 进程占用的内存容量明显大于 C++ 程序。这就是“省 cpu= 费内存”和“省内存 = 费 cpu”的经典难题,但我的目标就是打那小子的脸,所以费点内存也无所谓。
而我之所以能赢,是因为对方选择了一个幼稚的策略。其实他要想赢,最好的办法就是添加内存泄漏,故意把所有分配都保留在内存当中。这样 C++ 程序的内存占用量还是更小,但速度却比原先快得多。或者,他也可以用给栈分配缓冲区之类的设计来进一步提高性能,这种办法在实际生产中其实经常用到。
另外还有如何选择性能基准的问题。一般来说,大家比较的就是每秒操作数量。这里的 JS 对 C++ 就是个很好的例子,证明了“先理解总体性能成本,再做选择”往往更加靠谱。在软件架构中,我们必须得时刻关注资源层面的“总体拥有成本”。
转载请在文章开头和结尾显眼处标注:作者、出处和链接。不按规范转载侵权必究。
未经授权严禁转载,授权事宜请联系作者本人,侵权必究。
本文禁止转载,侵权必究。
授权事宜请至数英微信公众号(ID: digitaling) 后台授权,侵权必究。
评论
评论
推荐评论
暂无评论哦,快来评论一下吧!
全部评论(0条)