【外评】Why Not Rust?

我最近读了一篇批评 Rust 的文章,虽然它提出了一堆好观点,但我并不喜欢–这是一篇容易引起争论的文章。总的来说,我觉得不能推荐批评 Rust 的文章。这是一个遗憾–正视缺点是很重要的,而揭穿低劣的/错误的批判尝试则会对真正好的论点产生不利影响。

因此,我在此尝试反驳 Rust:

并非所有编程都是系统编程

Rust 是一种系统编程语言。它能精确控制数据布局和代码运行时的行为,为你提供最高的性能和灵活性。与其他系统编程语言不同的是,它还提供了内存安全性–有漏洞的程序会以定义明确的方式终止,而不是释放(可能对安全敏感的)未定义行为。

然而,在许多(大多数)情况下,人们并不需要极致的性能或对硬件资源的控制。在这些情况下,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 中,你需要做出一些选择,有些选择甚至重要到需要专门的语法。

所有这些复杂性都是有原因的–我们不知道如何创建一种更简单、内存安全的底层语言。但并非每项任务都需要低级语言来解决。

编译时间

编译时间是一切的乘数。用运行速度较慢但编译速度较快的编程语言编写的程序,运行速度会更快,因为程序员有更多时间进行优化!

Rust 在泛型困境中有意挑选了慢速编译器。这并不一定是世界末日(由此带来的运行时性能提升是真实的),但这确实意味着在大型项目中,你将不得不为合理的构建时间拼尽全力。

rustc 实现了可能是生产编译器中最先进的增量编译算法,但这感觉有点像与语言编译模型作斗争。

与 C++ 不同,Rust 的编译并不存在令人尴尬的并行性;并行性的大小受限于依赖关系图中关键路径的长度。如果你有 40 多个内核需要编译,这一点就会显现出来。

Rust 还缺乏 pimpl 成语的类比,这意味着更改 crate 需要重新编译(而不仅仅是重新链接)其所有反向依赖关系。

成熟度

Rust 诞生五年,绝对是一门年轻的语言。尽管它的未来看起来很光明,但我赌 “C 语言十年后还会存在 “的钱比赌 “Rust 十年后还会存在 “的钱要多(见林迪效应)。如果您要编写的软件能使用几十年,您就应该认真考虑选择新技术的风险。(但请记住,90 年代在银行软件中选择 Java 而不是 Cobol,事后证明是正确的选择)。

Rust 只有一个完整的实现–rustc 编译器。最先进的替代实现 mrustc 故意省略了许多静态安全检查。因此,它对 CPU 体系结构的支持范围比 C 语言要窄,C 语言有 GCC 实现和许多供应商专用的专有编译器。

最后,Rust 缺乏官方规范。参考文献正在编写中,还没有记录所有精细的实现细节。

替代语言

在系统编程领域,除了 Rust 之外还有其他语言,主要有 C、C++ 和 Ada。

现代 C++ 提供了提高安全性的工具指南。甚至有人提出了类似 Rust 的生命周期机制!与 Rust 不同的是,使用这些工具并不能保证不存在内存安全问题。现代 C++ 更安全,Rust 更安全。不过,如果你已经维护了大量的 C++ 代码,那么检查一下遵循最佳实践和使用 sanitizers 是否有助于解决安全问题是很有意义的。这很难,但显然比用另一种语言重写要容易得多!

如果使用 C 语言,您可以使用形式化方法来证明不存在未定义的行为,或者直接对所有内容进行详尽测试

如果不使用动态内存(永远不要调用 free),Ada 就是内存安全的。

Rust 是成本/安全曲线上一个有趣的点,但远非唯一的点!

 

工具

Rust 的工具有好有坏。基线工具、编译器和构建系统(cargo)经常被认为是一流的。

但举例来说,一些与运行时相关的工具(最明显的是堆剖析)并不存在–如果没有运行时,就很难对程序的运行时进行反思!此外,虽然集成开发环境的支持还算不错,但其可靠性远不及 Java。如今,Rust 无法对数百万行的程序进行复杂的自动重构。

集成

不管 Rust 的承诺是什么,当今的系统编程世界说的是 C 语言,居住的是 C 和 C++,这是不争的事实。Rust 并没有刻意模仿这些语言–它没有使用 C++ 风格的类或 C ABI。

这意味着两个世界之间的整合需要明确的桥梁。这些桥接并非天衣无缝。它们是不安全的,并不总是完全零成本的,而且需要在两种语言之间同步。虽然片段集成的总体承诺得以实现,而且工具也跟上了步伐,但在实现过程中还是会出现意外的复杂性。

一个具体的问题是,Cargo 固执己见的世界观(这对纯 Rust 项目来说是个福音)可能会让它更难与更大的构建系统集成。

性能

“使用 LLVM “并不是解决所有性能问题的通用方案。虽然我不知道 C++ 和 Rust 的大规模性能比较基准,但不难列出一系列 Rust 相对于 C++ 性能有所下降的情况。

其中最大的可能就是 Rust 的移动语义是基于值的(机器码级别的 memcpy)。相比之下,C++ 的语义使用的是可以窃取数据的特殊引用(机器代码级的指针)。理论上,编译器应该能看穿复制链,但实际上往往不能: #57077. 与此相关的一个问题是没有放置新数据–Rust 有时需要将字节复制到堆栈或从堆栈中复制,而 C++ 则可以就地构造。

有点可笑的是,Rust 的默认 ABI(为了尽可能提高效率,该 ABI 并不稳定)有时比 C 语言还糟糕: #26494.

最后,虽然理论上 Rust 代码应该由于明显更丰富的别名信息而更高效,但启用别名相关的优化会引发 LLVM bug 和误编译: #54878.

不过,我还是要重申,这些都是挑选出来的例子,有时情况并非如此。例如,std::unique_ptr 存在性能问题,而 Rust 的 Box 则缺乏。

一个潜在的更大问题是,Rust 通过定义时间检查泛型,表现力不如 C++。因此,一些 C++ 模板的高性能技巧在 Rust 中无法用漂亮的语法表达出来

不安全的含义

对于 Rust 来说,比所有权与借用(ownership & borrowing)更为核心的思想或许就是不安全边界(unsafe boundary)。通过在不安全代码块和函数后面划定所有危险操作,并坚持为它们提供安全的高层接口,可以创建一个既

  • 健全(非不安全代码不会导致未定义的行为)、
  • 和模块化(可分别检查不同的不安全代码块)的系统。

很明显,这一承诺在实践中是可行的:对 Rust 代码进行模糊测试会发现恐慌,而不是缓冲区超限。

但理论前景并不乐观。

首先,Rust 没有定义内存模型,因此无法正式检查给定的不安全块是否有效。虽然有 “rustc 所做或可能依赖的事情 “的非正式定义,也有正在开发的运行时验证器,但实际模型还在不断变化。因此,可能有一些不安全代码今天还能正常运行,明天就会被宣布无效,明年又会被新的编译器优化所破坏。

其次,我们还注意到,不安全代码块实际上并不是模块化的。功能足够强大的不安全代码块实际上可以扩展语言。两个这样的扩展单独使用可能没有问题,但同时使用就会导致未定义的行为: 观察等价和不安全代码

最后,编译器中还存在明显的错误

以下是我特意省略的一些内容:

  • 经济学(”招聘 Rust 程序员更难了”)–我觉得 “成熟度 “部分抓住了问题的本质,而这并不能归结为鸡生蛋还是蛋生鸡的问题。
  • 依赖性(”stdlib 太小/所有东西都有太多的依赖性”)–鉴于 Cargo 和语言的相关部分有多好,我个人认为这不是问题。
  • 动态链接(”Rust 应该有稳定的 ABI”)–我不认为这是一个有力的论据。单态化从根本上就与动态链接不兼容,如果你真的需要,还有 C ABI。我确实认为这种情况可以得到改善,但我不认为这种改善需要针对 Rust

本文文字及图片出自 Why Not Rust?

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注