作者 | Kevin Scott
Rust是一门极具争议性的语言。有许多创业公司的开发者甚至创始人都点名表示: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 <Bar> }
struct Foo { bar : Rc<Bar> }
struct Foo { bar: Arc<Bar> }
在 Kotlin 中,开始类 Foo(val bar: Bar) 并继续解决业务问题。在 Rust 中,需要做出一些选择,其中一些选择非常重要,需要专门的语法。
所有这些复杂性的存在都是有原因的——我们不知道如何创建更简单的内存安全的低级语言,尽管并不是每个任务都需要用低级语言来解决。
漫长的编译时间往往会压垮每一位程序员。用运行速度较慢但编译速度较快的编程语言编写的代码,通常更有机会运行得更快,因为程序员有更多时间来优化代码。
Rust 在通用性难题中故意选择了缓慢的编译器。这不一定是世界末日(因为由此产生的运行时性能增益是真实的),但这确实意味着在较大的项目中,开发者将不得不努力争取合理的构建时间。
rustc 实现了生产编译器中可能最先进的增量编译算法,但这感觉有点像与语言编译模型作斗争。详情可以移步这篇官网:
https://rustc-dev-guide.rust-lang.org/queries/incremental-compilation .html
与 C++ 不同,Rust 构建并没有笨拙地并行化,并行度受到依赖图中关键路径长度的限制。如果有 40 个以上的内核进行编译,则会显示此信息。
Rust 还缺乏类似 pimpl 的功能,这意味着更改 crate 需要重新编译(不仅仅是重新链接)其所有反向依赖项。
Rust 只有 8 年的历史,相较而言,Rust还算一门年轻的语言。创建这个新语言的目的是为了解决一个顽疾:软件的演进速度大大低于硬件的演进,软件在语言级别上无法真正利用多核计算带来的性能提升。
根据林迪效应,相信“C++ 将在未来十年内存在”的人要远多于对“Rust 将在十年内存在”的人。同样地,如果你编写的软件可以使用数十年,在选择新技术之前,往往会再三考虑与之相关的风险。
但慎重考虑并不代表放弃新技术。一个过去的案例就是,在 90 年代为银行软件选择 JAVA 而不是 Cobol 事实证明是正确的选择)。
Rust 目前只有一种完整的实现——rustc 编译器。另一个最佳替代实现,mrustc,有意省略了许多静态安全检查。rustc 目前仅支持一种生产就绪后端 - LLVM。因此,它对 CPU 架构的支持范围比 C 语言更窄,后者具有 GCC 实现以及许多特定于供应商的专有编译器。
最后,Rust 缺乏官方规范。参考文档正在开发中,尚未记录实现的所有细节。
在系统编程领域,除了 Rust 之外,还有其他一些语言,主要是 C、C++ 和 Ada。
现代 C++ 提供了提高安全性的工具和指南,甚至有人为C++提出了类似 Rust 的生命周期机制。但与 Rust 不同,使用这些工具并不能保证没有内存安全问题。但是,如果你已经维护了大量 C++ 代码,那么检查以下最佳实践和使用清理程序是否有助于解决安全问题是有意义的。这很困难,但显然比用另一种语言重写它要容易。
如果你使用C,你可以使用形式化方法来证明不存在未定义的行为,否则你只能详尽地测试一切。如果不使用动态内存(切勿调用 free),Ada 是内存安全的。
Rust 偏偏是成本/安全曲线上的一个有趣的权衡点,但肯定不是唯一的不可替代的点。
Rust 工具是值得点赞叫好的。基线工具、编译器和构建系统(cargo)通常被认为是一流的。
但是,例如,一些与运行时相关的工具(尤其是堆分析)目前还不存在——如果没有运行时工具,就很难分析程序的运行时。此外,虽然 IDE 支持不错,但它还远未达到 Java 级别的可靠性。如今,在 Rust 中不可能自动复杂地重构数百万行程序。
“使用 LLVM”并不是解决所有性能问题的通用方法。虽然我不知道 C++ 和 Rust 的大规模性能基准,但不难列出一些 Rust 不如 C++ 的性能问题。
最大的一个可能,是 Rust 的移动语义是基于值的(机器代码级别的 memcpy)。相比之下,C++ 语义使用特殊引用(机器代码级别的指针),可以在其中处理数据。
理论上,编译器应该能够看穿复制链,但实际上却常常做不到。要知道, 一个相关的问题是不放置新的——Rust 有时需要从堆栈复制字节,而 C++ 可以就地构造东西对象。
有趣的是,为了使其尽可能高效而不稳定,Rust 的默认 ABI有时比 C 更糟糕。
图片
最后,虽然理论上 Rust 代码应该由于更丰富的别名信息而更加高效,但启用与别名相关的优化可能会导致 LLVM 错误和错误编译。
但是,重申一下,这些都是个例,有时的情况恰恰相反。例如,Rust 的 Box 中不存在 std::unique_ptr 的性能问题。一个潜在的更大问题是 Rust 的定义时检查泛型不如 C++ 那样富有表现力。因此,一些高性能的 C++ 模板技巧很难在 Rust 中用漂亮的语法来表达。
也许跟“所有权”和“借用”相比,更核心的问题是不安全(Unsafe)的边界。通过界定Unsafe块和函数后面的所有不安全操作,并为它们提供安全的上层接口,可以创建一个兼具以下功能的函数:
一、可解释(非不安全(non-unsafe)的代码不会导致未定义的行为)。二、模块化(可以单独检查不同的不安全块)。
显然,这个承诺已经在实践中得到了证实:有bug的 Rust 代码不会造成缓冲区溢出。
当然,问题没那么简单,也不那么乐观。
首先,Rust 的内存模型没有定义,因此无法正式检查给定的不安全块是否有效。对于“rust-c 做什么或可能依赖什么”,有非正式的定义,运行时验证器正在开发中,但实际模型正在不断变化。因此,可能有一些unsafe的代码,今天虽然在实践中可用,但明天就可能会被声明为无效,并且在明年就会被新的编译器优化所破坏掉。
其次,据业内开发者的观察结果是,unsafe实际上并不是模块化的。足够强大的不安全块实际上可以扩展语言。两个这样的扩展,单独使用时可能没问题,但如果一起使用,可能会导致未定义的行为、观察到的等效性和不安全的代码。
原文链接:https://medium.com/@kevin_scott_/why-not-rust-1d257c6a07da