当前位置: 首页 > 科技观察

我的JavaScript比你的Rust还快

时间:2023-03-17 21:03:20 科技观察

作者JoshUrbane是一名从业多年的软件架构师,喜欢在社交媒体上分享技术观点。最近,他写了一篇文章,记录了他和一个新手开发者打赌赢的经历,“我的JavaScript比你的Rust快”的结论也是从这次打赌中得来的。他的故事或许可以说明运营战略在研发实践中的重要性。对我来说,做软件架构师最开心的事情就是能够引导开发者理解最新的概念,影响他们的技术判断。有的开发商不是很嚣张吗,用理论和现实打他们的脸;架构师还必须负责营造一种学习氛围,以娱乐和帮助年轻开发人员成长和成熟。最让我高兴的是,突然跳出来一个目瞪口呆的年轻开发者,想要挑战我的技术建议(从开发者的角度来看,架构师就是一帮总是给出“错误”建议的傻瓜),并赌上了自己的全部命运坚持认为他的方式更好。问题是,我已经这样做了很长时间,以至于我无需验证就知道问题的正确答案是什么。那么来来来,看看我手下的真章吧。我把这个故事记录下来,几年后整理成今天的文章。梭哈是一种“智慧”说实话,好几年没讲了,很多细节记不清了。一般情况是结合当时团队的知识储备、可用的工具库和原有的技术债。我的建议是让每个人都使用Node.js。一位新的初级开发人员对他刚获得的计算机科学学士学位充满信心,想通过“炫耀”来挫败我。他们听说我是辅修计算机专业的,所以他们认为我根本不懂计算机的基本原理。其实刚毕业的时候,我以为自己很懂,但是在这个行业工作久了,越来越觉得计算机系统就像魔法一样……他的自信不是没有道理的。这个结论犹如“C++比JavaScript快”,基本上属于业界共识。但作为一个典型的架构师,我仍然坚持“视情况而定”。更具体地说,“完全优化的C++确实比同级别优化的JavaScript运行得更快”,毕竟JavaScript有不可避免的执行开销(即便如此,我们也可以将代码编译成静态程序以获得接近C++的性能)).不管怎样,故事到这里就结束了,我们走吧。令人意外的是,JavaScript代码确实比C++版本快了一点,而且从架构设计的角度来看,JS版本可以由当前团队维护,不需要依赖其他部门的技术能力。没关系,我不是100%确定我是对的,但考虑到这个用例中的内存对象大小可能是动态的,而且年轻的开发人员确实没有经验,我愿意赌Bundle。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++的一个很好的例子,证明“在做出选择之前了解整体性能成本”往往更可靠。在软件架构中,我们必须时刻关注资源层面的“总拥有成本”。步入现代:Rust,拜托Rust是我目前最喜欢的语言之一。它提供了许多现代特性,速度快,具有良好的内存模型,并生成相当安全的代码。Rust当然并不完美,编译时间长,涉及到很多奇怪的语义,但总体来说还是值得推荐的。你可以灵活控制Rust中的内存管理,但它的“堆栈”内存始终遵循所有者模型(ownershipmodel),这也是其引以为豪的高安全性能的基础。我目前正在从事的一个项目是用Rust编写的FaaS(函数即服务)主机,它执行WASM(WebAssembly)函数。它可以快速安全地执行各种隔离功能,最大限度地减少FaaS的操作开销。它也很快,每个核心每秒能够处理90,000个简单请求。更重要的是,它的总内存占用只有20MB左右,可以说是相当夸张了。但这与Node.js与C++的赌注有什么关系?简单来说,我把Node.js当成一个“合理”的性能基准(Go是一个“梦幻”的基准,它的性能绝对比不上那些为Web服务设计的语言,所以不要降低维数在这里。),毕竟我们程序早期的C++版本性能确实不行,唯一的优势就是内存占用不到Node.js版本的十分之一。虽然让代码先运行然后优化它没有错,但在像C++这样的“快速”语言中输给JavaScript肯定非常令人沮丧。而我之所以敢原地stud,就是靠着明显瓶颈的基本判断。瓶颈是内存管理。每个guestfunction都分配了一个内存数组,但是在一个function内部分配内存,在functionmemory和hostmemory之间复制数据肯定会产生很大的性能开销。随着动态数据四处乱扔,分配器几乎受到来自各个方向的冲击。至于解决办法,作弊!添加一个堆,两个堆,三个堆...本质上,堆代表分配器用来管理映射的一部分内存。程序会申请N个内存单元,分配器在可用内存池中搜索这些单元(或者向宿主申请更多的内存)并存储哪些单元已经被占用,然后返回内存的位置指针。当程序用完内存时,它会通知分配器,分配器会更新映射以查看哪些单元现在再次可用。很简单,对吧?但是当我们需要分配一堆具有不同生命周期和大小的内存单元时,麻烦就来了。这势必会产生大量的碎片,进而放大分配新内存的成本。于是开始出现性能损失,毕竟allocator的功能太简单了,只是寻找可用的存储位置。显然,这个问题没有很好的解决办法。虽然有很多可选的分配算法,但它们还是有各自的取舍,需要我们根据用例的特点选择最合适的方法(你也可以像大多数开发者一样直接使用默认选项)。让我们谈谈作弊。作弊的方式不止一种:对于FaaS,我们可以每次运行释放dealloc,每次运行完成后清空整个堆;我们还可以在函数生命周期的不同阶段使用不同的分配器,比如明确区分初始化阶段和运行阶段。这样,无论是cleanfunction(每次运行,都会重置为相同的初始内存状态)还是statefulfunction(每次运行之间保留状态),都可以获得相应的优化内存策略。在我们的FaaS项目中,我们最终构建了一个动态分配器,它会根据使用情况选择分配算法,并且实际选择会在运行之间保持不变。对于“低使用率”函数(即大多数函数),只需使用一个带有指向下一个空闲槽的指针的简单堆栈分配器。调用dealloc时,如果该单元是堆栈中的最后一个单元,则回滚指针;如果它不是最后一个单元,则不执行。当函数完成时,指针将被设置为0(相当于Node.js在垃圾回收之前退出)。如果dealloc失败的次数和函数的使用量达到一定阈值,则对剩余的调用使用其他分配算法。因此,这种方案在大多数情况下可以显着加快内存分配。运行时使用另一个“堆”——主机(或函数共享内存)。它使用相同的动态分配策略并允许直接写入函数内存,绕过早期C++版本中的复制步骤。这允许I/O直接从内核复制到客户函数,绕过主机运行时,从而显着提高吞吐量。Node.js与Rust进行了优化,RustFaaS运行时最终比我们的Node.js参考实现快70%以上,同时使用的内存占用量不到十分之一。但这里的关键是“优化”,它的初始实现实际上比较慢。我们的优化还需要对WASM函数进行一些限制,这些函数在编译期间是完全透明的,并且很少不兼容。Rust版本最大的优点是内存占用小,节省下来的RAM可以用于缓存或分布式内存存储等其他用途。这意味着I/O开销进一步降低,生产运行效率更高,效果甚至比拉高CPU配置更明显。未来我们还有更多的优化计划,但主要是解决一些在宿主层安全影响比较大的问题。虽然它与内存管理或性能无关,但它仍然支持“Rust比Node快”党的观点。总结其实全文都写了,也不能得出特别明确的结论。这里只是一些粗浅的观点:内存管理很有意思,每一种方法都是权衡取舍。通过正确的策略,任何语言都可以获得巨大的性能提升。我还是建议大家根据自己的实际目标灵活使用Node.js和Rust,这里不做优劣评判。JavaScript的可移植性确实更好,特别适合云原生开发场景;但如果你特别注重性能,那么Rust可能是更好的选择。我一直在谈论JavaScript,但实际上我在这里指的是TypeScript。归根结底,大家还是要根据实际情况选择最合适的技术方案。我们越了解不同栈的不同特点,我们在选择的时候就会越从容。原文链接:https://medium.com/@jbyj/my-javascript-is-faster-than-your-rust-5f98fe5db1bf