Mojo 对 Rust:Mojo 真能比 Rust 还快?

admin 2025-04-08 186人围观 ,发现265个评论

作者|JackClayton

译者|核子可乐

策划|Tina

博文与性能基准

如果Mojo正式加入战局,那我相信Mojo无疑将最终胜出。Mojo取胜的原因,在于无需改变任何开发者已经熟知的范式。只需要稍加学习,就能获得惊人的性能表现。首先Mojo的编译速度很快,而且使用感受跟大家已经熟悉的语言非常接近,性能也跟Rust不相上下。唯一的问题就是怎么让更多人接受它。


昨天在MojovsRust上看到了@ThePrimeagen的直播:他说的没错。如果Mojo能够全面落地,那么对于从事AI工作的朋友们来说,我们再也不用在“userspace”里看到Rust了。Mojo的价值主张对于熟悉Python的机器学习工程师和数据科学家们都是种福音。

Mojo:我们的目标 Mojo真比其他语言更快吗?

@ThePrimeagen提出了一个重要问题:Rust向来以强大的底层性能而闻名,那Mojo要如何实现比Rust(和C++)更好的开箱即用性能?

新人用户在初次加入Discord时,最常提出的问题就是Mojo能比某其他语言快多少。其实任何基准测试的结果都会受到各种因素的影响,我们不可能单凭一项测试结果就认定某语言比另一种语言更快。更科学的提法,应该是与某语言相比,Mojo相应的开销是多少。Mojo的一大核心目标就是帮助开发者发掘硬件设备上的性能极限,同时以符合人体工学的方式为Python开发者提供熟悉的使用感受。

与Python等动态语言相比,编译语言允许开发者去除不必要的CPU指令,例如将对象分配到堆、引用计数和定期垃圾收集等。Mojo从C++、Rust和Swift中汲取了经验教训与最佳实践,可通过直接访问机器的方式回避此类开销。

Mojo对决Rust

这两种语言都可以使用LLVM来优化代码生成,也都允许使用内联汇编(当然,相信没人会真这么做),所以理论上二者在传统硬件上的性能潜力基本相当。

但真正的问题在于:在不清楚编译器工作细节的情况下,惯用/常规Mojo代码的性能跟非汇编语言专家编写的普通Rust代码相比,到底孰优孰劣?

默认情况下靠借用减少memcpy

在新用户学习Rust时,遇到的第一个陷阱往往是函数参数默认通过移动来获取对象。也就是说当我们将某些内容传递给函数并尝试重用时,会收到编译器错误提示:

Rust

fnbar(foo:String){}

fnmain(){

letfoo=String::from("bar");

bar(foo);

dbg!(foo);

}

Output

5|letfoo=String::from("bar");

|---moveoccursbecause`foo`hastype`String`,whichdoesnotimplementthe`Copy`trait

6|bar(foo);

|---valuemovedhere

7|dbg!(foo);

|^^^^^^^^^valueusedhereaftermove

dbg!这行会引发编译器错误,因为我们已经将foo移至bar函数当中。在Rust这边,这意味着foo会对字符串指针、大小和容量进行memcpy。在某些情况下,memcpy可以被LLVM优化掉,但也并非永远如此。而且除非大家明确了解Rust/LLVM编译器的工作方式,否则实际情况将难以预测。

Mojo则针对标准用例简化了这一概念:

Mojo

Oktomutateauniquelyownedvalue

fnmain():

letfoo=String("foo")

bar(foo^)

print(foo)#error:fooisuninitbecauseitwastransferredabove

折腾到这里,我们终于在移动后尝试使用foo时触发了编译器错误——没错,想骗过Mojo中的借用检查器还真不轻松!这样的默认设计明显更好,不仅效率更高,而且不会妨碍拥有动态编程背景的工程师。默认情况下,他们仍然会获得预期行为,同时尽量提升代码的性能表现。

无需使用Pin

在Rust当中,不存在值同一性的概念。对于指向其自身成员的自引用结构,一旦对象移动,则该数据可能会因继续指向内存中的旧位置而变得无效。这会造成复杂性激增,特别是在异步Rust的部分,其中future需要自我引用并存储状态,因此必须用Pin打包Self以保证它不会移动。但在Mojo这边,对象带有一个标识,因此引用将始终返回内存中的正确位置,无需程序员承担任何额外复杂性。总之,Mojo在设计上帮助程序员回避掉了很多复杂因素。

基于最先进的编译器技术

Rust于2006年启动,Swift则诞生于2010年,二者主要构建在LLVMIR之上。Mojo则亮相于2022年,基于MLIR构建而成——MLIR是比Rust上使用的LLVMIR方法更加现代的“下一代”编译器堆栈。这里还有一段历史:我们的CEOChrisLattner于2000年12月在大学里创立了LLVM,并从其多年来的演变和发展中学到了很多。他随后加入谷歌领导MLIR的开发,旨在支持公司的TPU及其他AI加速器项目。接下来,他继续利用从LLVMIR中学到的知识开启了下一步探索。

Mojo是首个充分利用到MLIR先进特性的编程语言,既可以生成优化度更高的CPU代码,也能支持GPU和其他加速器,而且统计速度也比Rust快得多。这是目前其他语言无法实现的优势,也是AI和编译器爱好者们痴迷Mojo的核心原因。他们能够针对奇特的硬件建立起奇特的抽象,而我们普通开发者则可以通过Python式的语法轻松加以使用。

出色的SIMD人体工学设计

CPU通过特殊的寄存器与指令来同时处理多位数据,这就是SIMD(单指令、多数据)。但从历史上看,此类代码的编写体验在人体工学层面来看非常丑陋且难以使用。这些特殊指令已经存在多年,但大多数代码仍未针对其进行过优化。所以谁能解决这种复杂性并编写出可移植的SIMD优化算法,谁就能在市场上脱颖而出,例如simd_json。

Mojo的原语在设计之初就考虑到了SIMD优先:UInt8实际上是一个SIMD[,1],即1元素的SIMD。以这种方式表示它不会产生性能开销,同时允许程序员轻松将其用于SIMD优化。例如,我们可以将文本拆分成64字节块,将其表示为SIMD[,64],再将其与单个换行符进行比较,从而找到每个换行符的索引。由于机器上的SIMD寄存器可以同时计算512位数据的运算,因此这种操作就能将此类运算的性能提高64倍!

或者举个更简单的例子,假设大家有一个SIMD[,8](2,4,6,8,16,32,64,128),那么只需简单将其乘以Float64(2),就能轻松提高性能。与单独将每个元素相乘比较,这种方法在大多数机器上能够将性能提高8倍。

LLVM(也就是Rust)具有自动向量化优化通道,但由于无法更改SIMD的内存布局和其他重要细节,所以其性能表现永远达不到理论层面的开发优化极限。但Mojo在设计之初就考虑到SIMD特性,因此编写SIMD优化的感受与编写普通代码非常相似。

EagerDestruction急切销毁

Rust的设计灵感来自C++的RAII(资源获取即初始化),就是说一旦对象超出范围,应用程序开发者不必分心释放内存,编程语言本身会自行处理。这是个非常好的范例,能在保障动态语言人体工学的前提下回避垃圾收集机制带来的性能缺陷。

Mojo则更进一步,它不会等待末尾作用域,而在最后一次使用对象时释放内存。这对AI场景非常有利,因为提前释放对象意味着提前释放GPU张量,因此可以在等量GPURAM中拟合更大的模型。这是Mojo的独特优势,程序员无需费心设计即可获得最佳性能。Rust借用检查器最初会将全部内容的生命周期延长至其作用域的末尾,借此匹配解构函数(destructor)的行为,但这会给用户带来一些令人困惑的后果。Rust随后添加了一些非词汇生命周期功能以简化开发者的工作。但凭借Mojo中的急切销毁(eagerdestructor)机制,这种简化效果可以直接实现,而且其与对象的实际销毁方式保持一致,因此不会引发令人难以理解的极端情况。

Rust中的另一种开销来自Drop的实现方式。它使用DropFlags标记跟踪是否应该在运行时删除对象。Rust在某些情况下能够实现优化,但Mojo可通过明确定义消除一切情况下的额外开销。

尾调用优化(TCO)

更新:社区讨论中指出,对于以下原始示例,Mojo可以正确优化所有内容,而Rust则因存在潜在bug而导致实现速度慢上许多。生成的程序集还显示,Rust会执行某种形式的TCO,即使对于堆分配的对象也会执行。考虑到这些,我更新了以下示例并调整了本章节的具体内容。

由于Mojo具有急切销毁机制,因此MLIR和LLVM能够更高效地执行尾调用优化。以下示例将两种语言中的递归函数与堆分配的动态向量进行了比较。请注意,这里只是简单示例,强调以尽可能少的代码演示二者间的差异。

首先运行cargonewrust,而后对./rust/src/做如下编辑:

./rust/src/

fnrecursive(x:usize){

ifx==0{

return;

}

letmutstuff=Vec::with_capacity(x);

foriin0..x{

(i);

}

recursive(x-1)

}

fnmain(){

recursive(50_000);

}

之后运行:

Bash

cdrust

cargobuild--release

cdtarget/release

hyperfine./rust

在M2Mac上的运行结果如下:

Output

Benchmark1:./rust

Time(mean±σ):2.119s±0.031s[User:1.183s,System:0.785s]

Range(min…max):2.081s…2.172s10runs

我们可以在同一文件夹中使用单一文件运行mojo版本,这里将其命名为:

fnrecursive(x:Int):ifx==0:returnvarstuff=DynamicVectorIntforiinrange(x):_back(i)recursive(x-1)

fnmain():recursive(50_000)

之后运行:

Bash

hyperfine./mojo

Output

Benchmark1:./mojo

Time(mean±σ):620.6ms±5.6ms[User:605.2ms,System:2.1ms]

Range(min…max):613.9ms…632.4ms10runs

编译器必须在适当时机调用析构函数。对Rust来说,也就是在值超出范围的时候。在递归函数中,Vec拥有一个析构函数,需要在每次函数调用后运行。也就是说该函数的堆栈帧无法如尾调用优化所需要的那样被丢弃或覆盖。而由于Mojo拥有急切销毁机制,因此不存在这一限制,能够通过堆分配的对象更有效地实现TCO优化。

使用valgrind--tool=massif分析两个版本的程序,能帮助我们更深入地理解此行为。这里切换至Linux云实例来运行本实验,在10GB峰值分配内存之下,Rust版本的平均运行时间为0.067秒;而在1.5MB的峰值分配内存下,Mojo版本的成绩则为1.189秒!如前所述,内存是AI应用场景下的重要资源,而急切销毁显然能帮助程序员在无需特别设计的情况下获取最佳性能。

总结

我们对Rust高度赞赏,Mojo的设计也在很大程度上其启发。Rust拥有系统编程语言领域最出色的高级人体工学设计,但正如@ThePrimeagen所指出,它在AI应用领域存在两大问题:

编译速度慢,而AI特别强调实验与快速迭代;

大多数有Python经验的AI研究人员不愿花时间从零开始学习一门新语言。我们团队的成员曾试图在谷歌通过“SwiftforTensorFlow”解决这个问题,但同样由于AI研究者不愿学习全新且编译速度较慢的语言,这套方案没能流行起来。我们也很喜欢Python/C++/Rust/Swift/Julia等语言,但它们都是拥有长期历史包袱的传统语言,所以轻装上阵的Mojo就成了应对这些古老挑战的唯一方法。

Mojo能够为系统工程师提供最佳性能,但距离为Python程序员提供符合期待的所有动态功能还有很长的路要走。就当前来讲,如果大家需要开发生产级别的应用程序,那么Rust仍是个不错的选择。但如果各位好奇心旺盛并更多面向未来,希望掌握一门可能在未来50年内对AI发展有所助益的语言,那不妨给Mojo个机会!我们将逐步将各种AI库添加至Mojo附带的软件包中,努力通过更多杀手级应用向世界展示Mojo的卓越能力。

最后期待大家加入Mojo社区大家庭,相关资源链接整理如下:

立即下载并使用Mojo()

阅读编程手册()并了解API()

探索GitHub上的相关示例()

加入我们的Discord社区()

参与MojoGitHub上的讨论()

订阅Modverse时事通讯()

原文链接:Mojo对Rust:Mojo真能比Rust还快?_编程语言_JackClayton_InfoQ精选文章

猜你喜欢
    不容错过