我为什么反对使用Rust?

本文最初发布于matklad的个人博客,由InfoQ中文站翻译并分享。

我最近读到一篇批评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<Bar>    }
struct Foo     { bar: Rc<Bar>     }
struct Foo     { bar: Arc<Bar>    }

在Kotlin中,你编写类class Foo(val bar: Bar),然后继续解决你的业务问题。在Rust中,你需要做出一些选择,其中一些非常重要,以至于需要专门的语法。

所有这些复杂性的存在都是有原因的——我们不知道如何创建一个更简单的内存安全的低级语言。但并不是所有的任务都需要低级语言来解决。

延伸阅读:为什么C++避免了“瓦萨”号沉没的命运?(https://www.youtube.com/watch?v=ltCgzYcpFUI)

编译时间

编译时间是所有事情的倍增器。如果一门语言运行速度慢但编译速度快,那么用它编写的程序也可以运行得更快,因为程序员将有更多的时间来做优化!

Rust在泛型困境中有意选择了慢速编译器。这并不是世界末日(最终的运行时性能改进是真的),但它确实意味着,在较大的项目中,你必须竭尽全力才能获得合理的构建时间。

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

与C++不同,Rust的构建并不是并行的;并行量受依赖图中关键路径的长度限制。如果你有40多个core需要编译,这就显出来了。

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

成熟度

Rust才五岁,绝对是一门年轻的语言。尽管它的未来看起来很光明,我还是会把更多的钱押在“C语言10年后仍将存在”上,而不是“Rust语言10年后仍将存在”上(参见林迪效应)。如果你编写的软件要用几十年,那么你应该认真考虑选择新技术伴随的风险。(但是请记住,在90年代,为银行软件选择Java而不是Cobol是正确的选择)。

Rust只有一个完整的实现——rustc编译器。最先进的替代实现mrustc有意省略了许多静态安全检查。目前,rustc只支持一个可用于生产的后端——LLVM。因此,它对CPU架构的支持比C更窄,C有GCC实现和许多特定于供应商的专有编译器。

最后,Rust缺乏官方规范。语言参考还在编写中,文档中也没有详细提供所有的实现细节。

替代语言

在系统编程领域,除了Rust外,还有其他语言,比较出名的有C、C++和Ada。

现代C++为提高安全性提供了工具指南。甚至有人提议,建立一个类似Rustlifetime的机制

!与Rust不同,使用这些工具并不能保证不出现内存安全问题。但是,如果你已经维护了大量的C++代码,那么还是检查下,这些代码是否遵循了最佳实践,并借助sanitizers检测安全问题。这很难,但显然比用另一种语言重写要容易得多!

如果你使用C,则可以使用正式的方法来证明没有未定义的行为,或者只是详尽地测试一切。

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

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

工 具

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

但是,举例来说,它缺少一些运行时相关的工具(最明显的是堆分析)——如果没有运行时,就很难反映程序的运行时状况!此外,尽管IDE支持很不错,但它的可靠性还远远达不到Java级别。在如今的Rust中,自动对数百万行程序进行复杂的重构是不可能的。

集 成

无论Rust的承诺是什么,如今的系统编程世界都是使用C语言,由C和C++所占据,这就是事实。Rust有意避免模仿这些语言——它不使用C++风格的类或C ABI。

这意味着,世界之间的整合需要明确的桥梁。这不是无缝的。它们unsafe,总会有点成本,并且需要在语言之间保持同步。虽然分段集成的承诺已经实现,工具也赶上了进度,但在此过程中会出现偶发的复杂性。

一个特别的问题是,Cargo固执的世界观(这对于纯Rust项目来说是件好事),但可能会使它更难与更大的构建系统集成。

性 能

“使用LLVM”并不是所有性能问题的通用解决方案。虽然我不知道在规模较大时,C++和Rust基准测试的性能对比,但是不难想出一组场景,在这些场景中,相对于C++,Rust还有一些性能问题没有解决。

最大的问题可能是,Rust的move语义是基于值的(机器代码级的memcpy)。相反,C++语义使用特殊的引用,你可以从中窃取数据(机器代码级的指针)。理论上,编译器应该能够识别副本链;实践中,情况通常并非如此:#57077。一个相关的问题是,没有placement new——Rust有时需要从栈复制字节/将字节复制到栈,而C++可以在适当的位置构造。

有点可笑的是,Rust的默认ABI(为使其尽可能高效而不稳定)有时比C更糟糕:#26494

最后,虽然从理论上讲,由于Rust具有丰富得多的aliasing信息,其代码应该更高效,但启用与aliasing相关的优化将触发LLVM Bug和编译错误:#54878

但是,重申一下,这些都是刻意挑选的例子,可能有失偏颇。例如,std::unique_ptr有一个性能问题,而Rust的Box则没有。

一个潜在的更大的问题是,Rust的定义时检查泛型比C++表达性差。因此,一些用于提高性能的C++模板技巧无法在Rust中使用好的语法进行表达。

不安全的含义

对于Rust而言,一个比ownership&borrowing更核心的概念可能是unsafe边界。将所有的危险操作都放在unsafe块和函数的后面,并为它们提供一个安全的高层接口,就有可能创建一个同时满足以下两点的系统:

  1. 健壮(非unsafe代码不会导致未定义的行为);

  2. 模块化(不同的unsafe块可以单独检查)。

很明显,在实践中,这种保证是有效的:对Rust代码进行模糊测试,发现隐藏的panics,避免缓冲区溢出。

但理论上,前景并不乐观。

首先,缺少Rust内存模型定义,就不可能正式检查给定的不安全块是否有效。对于“rustc所做的或可能依赖的事情”和正在开发中的运行时验证器,都只有非正式的定义,而实际的模型是不断变化的。因此,某些地方可能存在一些unsafe代码,这些代码今天还能正常工作,明天就可能被声明失效,并在明年被新的编译器优化破坏掉。

其次,还有一种观点认为,unsafe块实际上并不是模块化的。总之,功能足够强大的unsafe块可以扩展该语言。两个这样的扩展单独使用可能没有问题,但如果同时使用,则会导致未定义的行为:观测性等价和不安全代码

最后,编译器中还有公开的Bug

下面是我特意从清单中删除的一些内容:

  • 经济因素(“招聘Rust程序员更困难”)——我觉得“成熟度”一节抓住了问题的本质,它不能简化为鸡和蛋的问题。

  • 依赖关系(“stdlib太小/所有东西都有太多的依赖”)——考虑到Cargo以及语言的相关部分都很不错,我个人并不认为这是一个问题。

  • 动态链接(“Rust应该有稳定的ABI”)——我不认为这是一个强有力的论点。从根本上来说,单态化(Monomorphization)与动态链接是不兼容的,如果需要的话,还有C ABI。我确实认为这里的情况可以改善,但我不认为这种改善是Rust特有的

感兴趣的读者可以点击这里:https://www.reddit.com/r/rust/comments/iwij5i/blog_post_why_not_rust/ ,查看关于本文的讨论。

查看英文原文:

https://matklad.github.io/2020/09/20/why-not-rust.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章