最近我读了一篇批评 Rust 的文章,虽然它提出了一堆好的观点,但我并不认同它 -- 这是一篇容易引起争论的文章。总的来说,我不会推荐一篇批评 Rust 的文章。这是一个遗憾 -- 正视缺点是很重要的,但也需要反对那些草率的或者不准确失误的批判。
所以,下面是我力挺 Rust 的一些理由。
不是所有的开发都是系统编程
Rust 是一种系统编程语言。它提供了对数据布局和代码运行时行为的精确控制,赋予你最大的性能和灵活性。与其他系统编程语言不同的是,它还提供了内存安全--有bug的程序会以一种明确定义的方式终止,而不是出现(潜在的安全威胁)未定义的行为。
然而,在大多数情况下,人们并不需要终极性能或对硬件资源的极致控制。在这种情况下,像 Kotlin 或 Go 这样的现代可管理语言提供了不错的速度和令人羡慕的性能,并且由于使用垃圾回收器进行动态内存管理而保证了内存安全。
复杂度
程序员的时间是宝贵的,如果你选择了 Rust,预计会有一部分时间花在学习各种使用技巧上。Rust 社区倾注了大量的时间来创建各种高质量教程,但 Rust 语言很庞大。即使 Rust 能够为你提供价值,你也可能没有太多精力投入到语言专业知识的提升中。
Rust 提高控制力的代价是选择的魔咒。
- struct Foo { bar: Bar }
- struct Foo<'a> { bar: &'a Bar }
- struct Foo<'a> { bar: &'a mut Bar }
- struct Foo { bar: Box
} - struct Foo { bar: Rc
} - struct Foo { bar: Arc
}
在 Kotlin 中,你开始 class Foo(val bar: Bar),就可以继续解决你的业务问题了。在 Rust 中,你需要做出一些选择,有些重要到需要专门的语法。
所有这些复杂性的存在是有原因的 -- 我们不知道如何创建一个更简单的内存安全的低级语言,虽然并不是每个任务都需要用低级语言来解决。
另请参见《为什么C++在瓦萨号沉没时航行》。
https://www.youtube.com/watch?v=ltCgzYcpFUI
编译时间
编译时间是所有工作的倍数。用运行速度较慢但编译速度较快的编程语言编写的代码,可以有机会运行得更快,因为程序员可以有更多的时间去优化代码。
Rust 在通用性的难题中有意挑选了慢速编译器。这并不一定是世界末日(因为由此带来的运行时性能提升是实实在在的),但这确实也意味着在大型项目中,你将不得不为合理的构建时间而拼尽全力。
rustc 实现了可能是生产型编译器中最先进的增量编译算法,但这感觉有点像和语言编译模型打架。
https://rustc-dev-guide.rust-lang.org/queries/incremental-compilation.html
与 C++ 不同的是,Rust 的构建并不是尴尬的并行,并行的数量受限于依赖图中关键路径的长度。如果你有40多个 core 进行编译,这就会体现出来。
Rust 还缺少一个类似于 pimpl 的功能,这意味着改变一个 crate 需要重新编译(而不仅仅是重新链接)其所有的反向依赖。
pimpl 见: https://en.cppreference.com/w/cpp/language/pimpl
成熟度
只有 5 岁,Rust 绝对是一门年轻的语言。尽管它的前景灿烂,但我曾经在“C将在十年后存在”上下的赌注,要比“Rust 将在十年后存在”下的赌注多得多(参见 Lindy Effect)。如果你写的软件要持续几十年,你应该认真考虑选择新技术的相关风险。但请记住,90年代在银行软件上选择 Java 而不是 Cobol,回想起来,证明是无比正确的选择)。
Lindy Effect: https://en.wikipedia.org/wiki/Lindy_effect
Rust 目前仅有一个完整的实现 -- rustc 编译器。另一个最好的替代实现 mrustc,故意省略了许多静态安全检查。rustc 目前只支持一个生产就绪的后端 -- LLVM。因此,它对 CPU 架构的支持比 C 窄,C 架构有 GCC 实现,也有许多厂商特定的专有编译器。
最后,Rust 缺乏官方规范。参考文档是一个正在进行中的工作,还没有记录所有细致的实现细节。
可替代性
在系统编程领域,除了 Rust 之外,还有一些其他语言,主要有 C、C++ 和 Ada。
现代 C++ 提供了提高安全性的工具和准则。甚至有人提议建立类似 Rust 的生命周期机制。与 Rust 不同,使用这些工具并不能保证没有内存安全问题。然而,如果你已经维护了大量的 C++ 代码,那么检查一下遵循最佳实践和使用 sanitizer, 对于解决安全问题是有意义的。这很难,但显然比用另一种语言重写更容易。
如果你使用 C 语言,你可以使用形式化方法来证明没有未定义的行为,否则你只能详尽地测试一切。
Ada 是内存安全的,如果你不使用动态内存(永远不调用 free)。
Rust 是成本/安全曲线上的一个有趣的点,但肯定不是唯一的一个点。
工具
Rust 工具是值得称赞的东西。基线工具、编译器和构建系统(cargo),经常被认为是一流的。
但是,举例来说,一些与运行时相关的工具(最明显的是堆分析)目前还不存在 -- 如果没有运行时工具,就很难对程序运行时进行分析。此外,虽然 IDE 的支持还算不错,但远没有达到 Java 级别的可靠性。如今在 Rust 中,数百万行程序的自动复杂重构还做不到。
集成
不管 Rust 的愿景是什么,今天的系统编程世界还是 C 和 C++ 的天下,这是一个事实。Rust 有意不试图模仿这些语言 —— 它没有使用 C++ 风格的类或 C ABI。
这意味着,它们之间的集成需要明确的桥梁。这些都不是无缝的。它们是不安全的,并不总是零成本的,并且需要在语言之间同步。虽然片断式集成的一般也还能维持,工具也能赶上,但一路上却意外的复杂。
一个具体的小麻烦是,Cargo 独特的世界观(这对纯 Rust 项目来说是个福音)可能会使它更难与更大的构建系统集成。
性能
"使用LLVM" 并不是解决所有性能问题的通用方案。虽然我不知道 C++ 和 Rust 在规模上的性能的基准,但不难列出一个 Rust 不如 C++ 一些性能问题列表。
最大的一个可能是 Rust 的 move 语义是基于 value 的(机器代码级别的 memcpy)。相比之下,C++ 语义使用的是你可以使用数据的特殊引用(机器代码级的指针)。理论上,编译器应该能够看穿复制链,但在实践中往往不能。#57077. 一个相关的问题是没有放置 new -- Rust 有时需要从堆栈中复制字节,而 C++ 可以在原地构造东西。
57077 https://github.com/rust-lang/rust/issues/57077
有点有趣的是,Rust 的默认 ABI(为了使其尽可能高效,它并不稳定)有时比 C 更差。#26494.
https://github.com/rust-lang/rust/issues/26494#issuecomment-619506345
最后,虽然理论上 Rust 代码应该更高效,因为有更丰富的别名信息,但启用别名相关的优化会引发 LLVM bug 和误编译。#54878.
https://github.com/rust-lang/rust/issues/54878
但是,重申一下,这些都是挑出来的例子,有时候这些领域会有相反的情况。例如,std::unique_ptr 的性能问题在 Rust 的 Box 中就不存在。
一个潜在的更大的问题是,Rust 的定义时间检查的泛型,表现力不如 C++。所以,一些高性能的 C++ 模板技巧,在 Rust 中就很难用漂亮的语法来表达。
模板技巧 http://eigen.tuxfamily.org/index.php?title=Expression_templates
Unsafe 的定义
一个比所有权和借用更核心的问题也许是 unsafe 的边界。通过在 unsafe 的块和函数后面划定所有危险的操作,并为它们提供安全的上层接口,就有可能创建一个既是:
- 合理的(非不安全的代码不能导致未定义行为)。
- 和模块化 (不同的不安全块可以分别检查)。
很明显,这个承诺在实践中得到了验证:有问题的 Rust 代码会带来 panic,而不是缓冲区超限。
但在理论上来看,问题就不那么乐观了。
首先,没有 Rust 内存模型的定义,所以无法正式检查给定的不安全块是否有效。有非正式的 "rustc 所做的或可能依赖的事情 "的定义,并且在进行中的运行时验证器,但实际的模型是在变化的。所以可能有一些不安全的代码,今天在实践中还可以用,明天可能就被宣布无效,明年又被新的编译器优化打破。
其次,还有一个观察,不安全块其实不是模块化的。足够强大的不安全块实际上可以扩展语言。两种这样的扩展单独使用可能是好的,但如果同时使用会导致未定义的行为,观测等价性和不安全代码。
见: https://smallcultfollowing.com/babysteps/blog/2016/10/02/observational-equivalence-and-unsafe-code/
最后,编译器中存在 bug。
见:https://github.com/rust-lang/rust/issues?q=is%3Aopen+is%3Aissue+label%3A%22I-unsound+%F0%9F%92%A5%22
下面是我刻意忽略的一些东西。
- 经济学:("雇用 Rust 程序员很难了")--我觉得 "成熟度 "部分抓住了它的本质,它不能还原成鸡和蛋的问题。
- 依赖性:("stdlib太小/所有东西都有太多的依赖")-- 考虑到 Cargo 和语言的相关部分有多好,我个人不认为这是一个问题。
- 动态链接:("Rust 应该有稳定的ABI")--我不认为这是一个强有力的论点。单态化与动态链接在根本上是很不兼容的,如果真的有需要,还有 C ABI 可用。我确实也认为这里有可改善空间,但我不认为这种改善需要针对 Rust。见:https://internals.rust-lang.org/t/a-stable-modular-abi-for-rust/12347/10?u=matklad
英文原文:
https://matklad.github.io/2020/09/20/why-not-rust.html
本文转载自微信公众号「高可用架构」,可以通过以下二维码关注。转载本文请联系高可用架构公众号。