谷歌翻译,少量修改。

原文:http://dtrace.org/blogs/bmc/2018/09/18/falling-in-love-with-rust/

让我以道歉为前言:这是一个技术爱情故事,因此,它是漫长的,漫无边际的,多愁善感的和个人的。 同样适合爱情故事,它有一个当哈利遇见莎莉的感觉,因为它的起源是不吉利的……

第一次遇到

十多年前,我参与了一项技术,竞争对手付出了最高的赞誉:他们试图实施自己的仿冒品。 因为这是在公开场合完成的(并且因为看着自己的作品被模仿而独特迷人),所以我花了太多时间跟踪他们的邮件列表并跟踪他们的进度(是的,他们在偶尔的不和中采取特别可耻的喜悦) 。 在他们的团队中,有一位技术专家显然非常有能力 - 当他选择在项目的生命中相对较早地离开团队时,我承认他感到宽慰。 这完全是在2005年; 多年来,Rust一直是“Graydon为了继续工作而消失的东西。”从我当时读到的描述来看,Graydon的新项目似乎非常雄心勃勃 - 而且我认为很少会有它,尽管当然不是因为缺乏能力或努力……

快进八年到2013年左右。 令人印象深刻的是,Graydon的Rust不仅还活着,而且它已经聚集了一个社区并得到了相当多的关注 - 足以值得认真看待。 似乎有一些非常有趣的想法,但当我得知Rust采用了M:N线程模型时,我可能已经坦率地消失了任何崭露头角的兴趣 - 包括像分段堆栈这样的更为巴洛克式的后果。 根据我的经验, 采用M:N模型的每个系统都对它感到后悔 - 而且很遗憾有一个有希望的新系统似乎不知道它本来可以承受的伤痕累累的肩膀。 对我而言,其影响大于这个单一的决定:我担心这可能表明更深层次的不适会使Rust不适合我喜欢写的基础设施软件。 因此,虽然Rust的雄心勃勃的愿景在任何方面都取得了成果,但我认为Rust不是为了我个人 - 而且我没有想到更多……

一段时间后,发生了一件非常奇妙的事情: Rust撕掉了它 。 Rust 删除分段堆栈的原因是简洁而彻底的诅咒; 他们删除M:N的理由是清晰,有思想和反思 - 但其决心也是毫不含糊的。 突然之间,鲁斯特变得非常有趣:所有系统都犯了错误,但很少有人鼓起勇气去纠正它们; 仅在此基础上,鲁特就成了一个值得密切关注的项目。

所以几年后,在2015年,我非常感兴趣地了解到亚当开始尝试使用Rust 。 在第一次阅读Adam的博客文章时,我认为他会通过从他的计算机中删除Rust编译器来结束看似痛苦的痛苦(如果不是移动到佛蒙特州的一个社区 ) - 但是当他最终非常积极时,Adam让我感到惊讶关于Rust,尽管经历了艰难的经历。 特别是,亚当欢呼像所有权模式这样重要的新思想 - 并明确希望他的经验可以作为警告他人以不同的方式接触语言。

在那之后的几年里,Rust继续成熟,我的好奇心(我敢说, 许多软件工程师的好奇心)已经稳步增强:我发现的越多,我就越感兴趣。 这种兴趣恰逢我个人寻求为我职业生涯的后半部分找到一种编程语言:正如我在2017年节点峰会上提到的平台作为价值观的反映 ,我一直在寻找一种反映我个人工程的语言关于稳健性和性能的价值观。 这些价值观反映了我内心深处的意义:软件可以是永久性的 - 软件的独特双重性,因为信息和机器都能提供永恒的完美和实用性,与其他人类的努力不同。 在这方面,我相信(并且仍然相信)我们生活在一个软件的黄金时代,这个软件将产生可以忍受世世代代的文物。 当然,当我们看起来像是在我们的贩卖的漂浮物中的腋窝时,很难保持这种令人兴奋的想法,匆忙实施的草率抽象淹没了。 在当前的语言中,只有Rust似乎与永久性共享这种愿望,其观点明显大于自身。

冒险

所以我一直在积极寻找机会深入研究Rust,今年早些时候,有人展示了自己:有一段时间,我一直在研究一种新的系统可视化机制,我称之为状态图 。 用于渲染状态图的软件需要吸入数据流,将其合并到合理的大小,并将其渲染为可由用户操纵的动态图像。 这最初是以node.js编写的,但是性能成了一个问题(特别是对于大型数据集),我做了Joyent在这种情况下所做的事情:我在C中重写了热循环,然后将其放入node.js附加组件(允许SVG呈现代码保留在JavaScript中)。 这很好,但很痛苦:C很简单,但是桥接到node.js的胶水代码就像往常一样反复无常,乏味且容易出错。 鉴于性能限制,对更高级别语言的强大功能的渴望,以及软件的实验性质,状态图为在Rust中重新实现的优秀候选者做出了贡献; 我的强烈好奇心终于得到了满足!

正如我所说的那样,我有一个好处就是看过(如果远道而来)许多其他人第一次遇到Rust。 如果那些岁月生锈的东西教会了我什么,那就是早期的日子可以像滑雪板或帆板运动的第一天:很多痛苦的摔倒! 所以我对Rust采取了刻意的做法:而不是在学习一门新语言和修补程序存在时不做的事情,我真的坐下来学习 Rust。 坦率地说,这是我的偏见(我总是寻找创作的第一原则,正如其创作者所解释的那样),但是对于Rust,我走得更远:我不仅购买了规范参考文献(Steve Klabnik撰写的Rust编程语言 , Carol Nichols和社区贡献者),我还买了一本带有更多叙述的O’Reilly书(由Jim Blandy和Jason Orendorff 编写Rust )。 在后一本书中,我做了一些我从未做过的事情,因为当天在Enter杂志中抄袭了BASIC程序:我在介绍性章节中输入了示例程序。 我发现这是非常有价值的:它让手指和大脑变得温暖,同时仍然吸收了Rust的新想法 - 调试我不可避免的转录错误让我能够理解我正在打字的内容。 最后实际上做了一些事情,并且(重要的是),通过使用已经正确的程序,我能够无痛地感受到Rust的一些巨大希望。

受到这些早期(如果温和)经历的鼓舞,我进入了我的状态图重写。 花了一点时间(是的,我和借用检查员发生了一些争执!)但是我对在Rust中重写状态图的快乐感到震惊。 因为我知道很多人都是我不久前所占据的(也就是对Rust的强烈疑惑,但也对其学习曲线持谨慎态度 - 并担心攀登它所需的时间和精力的投入),我会喜欢扩展一些我喜欢Rust 之外的东西,而不是所有权模型。 这不是因为我不喜欢所有权模式(我绝对这样做)或者所有权模型不是Rust的核心(它被正确地认为是Rust的震中),但因为我认为它的绝对规模有时使其他人相形见绌。 Rust的属性 - 我发现非常引人注目的属性! 在某种程度上,我正在为过去的自我写这篇文章 - 因为如果我对Rust有一点遗憾,那就是我没有看到超越所有权模型以便更早地学习它。

我将大致按照我发现它们的顺序讨论这些属性(明显的?)警告,这不应该被认为是权威的; 我对Rust仍然非常新,我提前为任何技术细节道歉,我错了!

1 Rust的错误处理很漂亮

关于Rust真正让我印象深刻的第一件事就是它的错误处理 - 但要理解为什么它如此引起我的共鸣需要一些额外的背景。 尽管它具有明显的重要性,但错误处理是我们在系统软件中没有真正做到的事情。 例如, 正如Dave Pacheo关于node.js观察到的那样 ,我们经常会混淆不同类型的错误 - 即编程错误(即我的程序由于逻辑错误而被破坏)和操作错误(即,错误条件外部的错误)我的程序已经发生,它影响了我的操作)。 在C中,这种混淆是不寻常的,但你可以看到它与臭名昭着的SIGSEGV信号处理程序,众所周知,该处理程序在截止日期之前潜入一个以上的本科项目,以应对其他不可饶恕的情况。 在Java世界中,对于捕获java.lang.NullPointerException或以其他方式尝试根据明显破坏的逻辑来驱动的(不赞成的)行为,这稍微更常见。 在JavaScript世界中,这种混淆是司空见惯的 - 并且是对承诺最严重的反对之一 。

除了本体论上的混淆之外,错误处理还存在臭名昭着的机械问题:对于可能返回值但也可能失败的函数,调用者如何描述这两个条件? (这被称为Lisp构造之后的半无差别问题 。)C处理这个因为它处理了很多东西:把它留给程序员来弄清楚它们自己的(坏)约定。 一些使用sentinel值(例如,Linux系统调用将返回空间切换为两个,并使用负值表示错误条件); 一些返回成功和失败的定义值,然后设置正交错误代码; 当然,有些人只是默默地吃错误( 甚至更糟 )。

C ++和Java(以及之前的许多其他语言)试图用异常的概念来解决这个问题。 我不喜欢例外:由于与Dijkstra在他反对“goto”的着名箴言中没有不同的原因,我认为例外有害 。 虽然从函数签名的角度来看它们可能很方便,但异常允许错误在伏击中等待,深入隐藏依赖的高草丛中。 当错误发生时,更高级别的软件可能不知道是什么击中了它,更不用说从谁那里 - 并且突然出现操作错误已成为程序错误。 (Java尝试用经过检查的例外来缓解这种偷袭行为,但在善意的情况下,它们在实践中存在严重缺陷 。)在这方面,例外是交易软件开发速度的具体例子及其长期可操作性。 作为一种手艺,我们最深刻,最基本的问题之一就是我们将“速度”置于首要位置,故意使自己对gimcrack软件的长期后果感到茫然。 例外情况通过允许他们假装错误是别人的问题来优化开发人员 - 或者可能根本不会发生错误。

幸运的是,异常不是解决此问题的唯一方法,其他语言采用其他方法。 像JavaScript这样的关闭密集的语言提供像node.js这样的环境,可以将错误作为参数传递 - 但是这个参数可以被忽略或者被滥用(并且无论如何都是无类型的),这使得这个解决方案远非完美。 并且Go使用它对多个返回值的支持(按照惯例) 返回结果和错误值 。 虽然这种方法肯定是对C的改进, 但它也很嘈杂,重复且容易出错 。

相比之下,Rust采用了一种在面向系统的语言中独一无二的方法:利用第一代数数据类型 - 其中一个东西可以是枚举类型列表中的一个,并且程序员需要明确其类型来操纵它 -然后将其与对参数化类型的支持相结合。 总之,这允许函数返回两种类型之一:一种表示成功,一种表示失败。 然后调用者可以对返回的类型进行模式匹配 :如果它是成功类型,它可以获取底层事物(通过展开它),如果它是错误类型,它可以获得基础错误并处理它,传播它或改进它(通过添加额外的上下文)并传播它。 它不能做什么(或者至少不能隐含地做)只是忽略它:它必须以某种方式明确地处理它。 (有关所有详细信息,请参阅带结果的可恢复错误 。)

为了使这个具体,在Rust中你得到的代码如下所示:

  1. fn do_it(filename: &str) -> Result {
  2. let stat = match fs::metadata(filename) {
  3. Ok(result) => { result },
  4. Err(err) => { return Err(err); }
  5. };
  6. let file = match File::open(filename) {
  7. Ok(result) => { result },
  8. Err(err) => { return Err(err); }
  9. };
  10. /* ... */
  11. Ok(())
  12. }

这已经非常好了:它比多个返回值更清晰,更强大,返回标记和异常 - 部分原因是类型系统可以帮助您正确识别。 但它也很冗长,所以Rust通过引入传播运算符更进了一步:如果你的函数返回一个Result ,当你调用一个自己返回Result的函数时,你可以在函数调用上附加一个问号来表示如果确定 ,结果应该被解包并且表达式成为未包装的东西 - 并且在Err上应该返回错误(并因此传播)。 这比解释更容易看到! 使用传播运算符将上面的示例转换为:

  1. fn do_it_better(filename: &str) -> Result {
  2. let stat = fs::metadata(filename)?;
  3. let file = File::open(filename)?;
  4. /* ... */
  5. Ok(())
  6. }

对我来说,这很美:它很健壮; 它是可读的; 这不是魔术。 它是安全的,编译器帮助我们达到这个目标,然后防止我们偏离它。

平台反映了它们的价值 ,我敢说传播操作符是Rust的一个体现:平衡优雅和表现力以及稳健性和性能。 这种平衡体现在Rust社区经常听到的口头禅中: “我们可以拥有美好的东西。”也就是说:虽然历史上这些价值观中的一些处于紧张状态(即,使软件更具表现力可能会隐含地减少稳健和性能)。 通过创新,Rust正在寻找不会为了另一个而牺牲其中一个价值的解决方案。

2 这些宏令人难以置信

当我第一次学习C时,我(正确地)警告我不要使用C预处理器。 但是,就像我们年轻时提醒过的许多事情一样,这个警告是明智的给予热心者预防伤害的警告; 事实更为微妙。 事实上,当我成为一名C程序员时,我不仅开始使用预处理器,而且依赖它。 是的,它需要仔细使用 - 但在正确的手中它可以生成更清洁,更好的代码。 (实际上,预处理器是我们实现DTrace静态定义跟踪的方式的核心。)所以,如果有的话,我对预处理器的问题不是它的危险,而是它的许多限制:因为它实际上是一个预处理器和没有内置到语言中,有各种各样的事情,它永远无法做到 - 比如访问抽象语法树。

有了Rust,我很高兴它对卫生宏的支持 。 这不仅解决了基于预处理程序的宏的许多安全问题,而且还允许它们非常强大:通过访问AST,宏可以提供几乎无限的语法扩展 - 但是使用指示符(尾随爆炸)调用程序员在使用宏时会清楚说明。 例如, Programming Rust中一个完全有效的例子是json! 允许在Rust中轻松声明JSON的宏。 这符合 Rust的人体工程学 ,并且有许多宏(例如, 格式! , vec!等)使Rust更加舒适。

宏的另一个优点:它们非常灵活和强大,可以进行有效的实验。 例如,我非常喜欢的传播操作符实际上是一次try! macro; 该宏被普遍使用(并且成功地),允许考虑基于语言的解决方案。 语言可能(并且已经!)因语言中发生的太多实验而不是如何使用而破坏; 通过其丰富的宏,似乎Rust可以使语言的核心保持更小 - 并确保当它扩展时,它是出于正确的原因和正确的方式。

3 format! 很高兴

好吧,这是一个小的,但它是(另一个)使得Rust非常愉快的小小的愉快之一。 许多(大多数?全部?)语言具有近似或等效的古老sprintf ,其中变量输入根据格式字符串格式化。 Rust的变体是format!(由println! , panic!等调用),并且(与Rust的更广泛的主题之一保持一致)感觉它已经从它之前的许多东西中学到了。 它是类型安全的(当然),但它也很干净,因为{}格式说明符可用于实现Display trait的任何类型。 我也喜欢{:?}格式说明符表示应该调用参数的Debug trait实现来打印调试输出。 更一般地说,所有格式说明符都映射到特定的特征 ,从而可以优雅地处理历史上的问题。 还有很多其他的细节 ,而且这些都是Rust如何使用宏来提供漂亮的东西而不会玷污语法或其他特殊外壳的具体例子。 没有任何格式化功能对Rust来说是独一无二的,但重点在于:在这个(小)域中(如同许多),Rust感觉就像是对它之前的最佳作品的升华。 任何不得不忍受我的一次谈话的人都可以证明,我相信,欣赏历史对理解我们的现在和描绘我们的未来至关重要。 Rust似乎以最好的方式拥有这种观点:它是过去的虔诚而不被它监禁。

4 include_str! 是天赐之物

状态映射代码的一个肮脏方面是它有效地封装了另一个程序 - 一个存在于SVG中的JavaScript程序,以允许状态映射的交互性。 此代码存在于自己的文件中,statemap代码应将其传递给生成的SVG。 在node.js / C混合中,我被迫在文件系统中找到该文件 - 这很烦人,因为它必须与二进制文件一起交付并定位等等。现在Rust - 像许多语言(包括ES6)一样 - 支持原始字符串文字。 顺便说一句,有趣的是看到讨论导致其增加 ,特别是,一群人如何真正看到每种语言,以便看到应该模仿的东西与可以改进的东西。 我非常喜欢Rust收敛的语法: r后跟一个或多个octothorpes,后跟一个引号开始一个原始字符串文字,一个引号后跟一个匹配数量的octothorpes,然后结束一个文字,例如:

  1. let str = r##""What a curious feeling!" said Alice"##;

仅这一点就可以让我做我想做的事,但仍然有点遗憾,因为它是一堆生活在.rs文件中的原始文字中的JavaScript。 输入include_str! ,这允许我告诉编译器在编译期间在文件系统中找到指定的文件,并静态地将其放入我可以操作的字符串变量中:

  1. ...
  2. /*
  3. * Now drop in our in-SVG code.
  4. */
  5. let lib = include_str!("statemap-svg.js");
  6. ...

太赞了! 多年来,我多次想要我的C,这是另一个让Rust变得如此令人耳目一新的小东西(但很重要!)。

5 Serde非常好

Serde是一个允许序列化和反序列化的Rust包 ,它非常好。 它使用宏(特别是Rust的过程宏 )来生成用于序列化和反序列化的特定于结构的例程。 因此,Serde需要非常少的程序员使用并且表现得非常好 - 这是Rust反复蔑视传统智慧的具体体现,程序员必须在抽象和性能之间做出选择!

例如,在statemap实现中,输入是以元数据有效负载开头的连接JSON。 要在Rust中读取此有效负载,我定义了结构,并表示我希望派生由Serde实现的Deserialize特征 :

  1. #[derive(Deserialize, Debug)]
  2. #[allow(non_snake_case)]
  3. struct StatemapInputMetadata {
  4. start: Vec<u64>,
  5. title: String,
  6. host: Option<String>,
  7. entityKind: Option<String>,
  8. states: HashMap<String, StatemapInputState>,
  9. }

然后,实际解析它:

  1. let metadata: StatemapInputMetadata = serde_json::from_str(payload)?;

而已。 由于传播运算符的神奇之处,错误得到了妥善处理和传播 - 它为我处理了繁琐,容易出错的事情,比如某些成员的可选性(本身通过Rust无处不在的Option类型表达得非常漂亮)。 有了这一行代码,我现在(强有力地)拥有一个可以使用和操作的StatemapInputMetadata实例 - 而且这一点在这一切上表现得非常好。 在这方面,Serde代表了最好的软件:它是一个复杂,复杂的实现,提供优雅,强大,高性能的抽象; 作为传奇的白袜队播放播音员Hawk Harrelson可能会说, MERCY!

6 我喜欢元组

在我的C中,我已经知道在函数中声明匿名结构 。 更一般地说,在任何强类型语言中,有很多时候你不想填写文书工作来构建你的数据:你只是想为一个小工作增加一些结构。 为此,Rust在元组中借用了ML的古老结构。 元组表示为括号列表,它们基本上按照您期望的方式工作,因为它们在大小和类型上是静态的,您可以索引到任何成员。 例如,在一些需要确保正确解释颜色名称的测试代码中,我有:

  1. let colors = vec![
  2. ("aliceblue", (240, 248, 255)),
  3. ("antiquewhite", (250, 235, 215)),
  4. ("aqua", (0, 255, 255)),
  5. ("aquamarine", (127, 255, 212)),
  6. ("azure", (240, 255, 255)),
  7. /* ... */
  8. ];

然后颜色[2] .0 (比如说)将是字符串“aqua”; (colors [1] .1)。2将是整数215.不要让上面没有类型声明欺骗你:元组是强类型的,只是Rust正在推断我的类型。 因此,如果我不小心尝试(比方说)在上面的向量中添加一个元素,其中包含一个不匹配签名的元组(例如,元组“ ((188,143,143),(”rosybrown“)) ”“,其中包含订单反转),Rust会给我一个编译时错误。

元组的完全集成使它们成为一种使用的乐趣。 例如,如果函数返回一个元组,您可以轻松地将其组成部分分配给不相交的变量,例如:

  1. fn get_coord() -> (u32, u32) {
  2. (1, 2)
  3. }
  4. fn do_some_work() {
  5. let (x, y) = get_coord();
  6. /* x has the value 1, y has the value 2 */
  7. }

好东西!

7 紧密结合的测试非常棒

我对DTrace的一个遗憾是,我们在启动项目的同时没有启动DTrace测试套件 。 甚至在我们开始建造它之后(太晚了,但在我们发货之前很幸运),它仍然远离源头已经存在了好几年。 即使是现在,跑步也有点痛苦 - 你真的需要知道它在那里。

这代表了用C测试的所有错误:因为它需要定制机器,太多人不打扰 - 即使他们知道更好! Viz。:在原始状态图实现中,没有测试代码 - 并不是因为我不相信它,而是因为它对于相对较小的东西来说太过分了。 是的,有很多针对C和C ++的测试框架,但根据我的经验,集成框架过于紧缩 - 同样,对于较小的项目而言,这是不值得的。

随着测试驱动开发的兴起,许多语言采用了更加集成的测试方法。 例如,Go拥有合理称赞的测试框架 ,Python具有单元测试等。Rust采用高度集成的方法 ,结合了所有世界中最好的方法 :测试代码与它正在测试的代码一起生效 - 但无需使代码弯曲到重量级框架。 这里的主要工具是条件编译和Cargo,它们共同使编写测试和运行它们变得如此容易,我发现自己使用状态图进行真正的测试驱动开发 - 即在开发代码时编写测试。

8 这个社区很棒

根据我的经验,最好的社区是那些在其成员中具有包容性但却坚持共同价值观的社区。 当社区不具有包容性时,它们会停滞不前,或腐烂(或更糟); 当社区不分享价值观时,他们会发生争执和破裂。 这可能是一个非常棘手的平衡,特别是当许多开源项目开始时是一个人的工作:社区很难不反映其创始人的特质。 这很重要,因为在开源时代,社区至关重要:一个是选择一个社区,就像选择一个技术一样,每个都通知另一个社区的未来。 我更重视的一个因素是严格的尺寸:我最喜欢的一些社区是小的 - 而我最不喜欢的一些是巨大的。

出于社区的目的,Rust拥有清晰明确,广泛共享的价值观,并且经常突出并经常重申。 如果您前往Rust网站,这是您将阅读的第一句话:

Rust是一种系统编程语言,运行速度极快,可以防止段错误并保证线程安全。

这是正确的:它说作为一个社区,我们重视绩效和稳健性 - 我们相信我们不应该在这两者之间做出选择。 (而且我们已经看到这不仅仅是修辞,因为许多Rust的决定表明这些价值观确实是该项目的明星。)

而就包容性而言,显而易见的是,您可能会用您的母语阅读该价值陈述,因为Rust网页已被翻译成十三种语言。 事实上,它被翻译成这么多语言,使得Rust在同行中几乎是独一无二的。 但也许更有趣的是,这种全球包容性观点可能会找到它的根源:在其同行的网站中, 只有Ruby同样是本地化的 。 鉴于Steve Klabnik和Carol Nichols等几位杰出的Rustaceans来自Ruby社区,因此猜测他们带来了这种全球包容性观点并不是不合理的。 这种包含是在Rust社区中一次又一次地看到的:来自不同语言和不同背景的不同观点。 那些来到鲁斯特的人带来了来自古老国家的经历 - 好的和坏的 - 结果是一大堆思想。 这是一种深刻的包容性:通过将这些不同的观点融入社区,然后将它们与共同的价值观和共同的目标结合起来,Rust实现了丰富而富有成效的思想异质性。 也就是说,因为社区同意大事(即其基本价值观),所以它对于较小的事物具有建设性的不同意(即达成共识)的空间。

这并不是说这很容易! 在RustConf 2018的开幕主题演讲中查看Ashley Williams,了解在实践中如何通过这些较小的差异来解决问题。 Rust比“传统的”BDFL模式走得更艰难,但它在质量上更好 - 我相信我喜欢Rust的许多事情都反映了(以及对其强大的社区的贡献)。

9.性能突破C

最后,我们在Rust奥德赛中发现了最后一件事 - 但在很多方面,这是最重要的一件事。 正如我在内部演示文稿中所描述的那样,我在Rust中经历了一些令人沮丧的尝试,我在Rust中实现了与C相同的结构。因此,我精神上放弃了性能,决定先获得一些工作,然后再进行优化。

我确实让它工作,并且能够对它进行基准测试,但是为了给出一些数字的上下文,这里是时候在旧的(慢)纯node.js实现中生成一个状态图,用于适度的跟踪 (229M,〜我的2.9 GHz Core i7笔记本电脑上的3.9M状态转换):

%time ./statemap-js/bin/statemap ./pg-zfs.out> js.svg

真实1m23.092s
用户1m21.106s
sys 0m1.871s
这很糟糕 - 较大的输入会导致内存不足。 这里的版本重新实现为C / node.js混合:

%time ./statemap-c/bin/statemap ./pg-zfs.out> c.svg

真正的0m11.800s
用户0m11.414s
sys 0m0.330s
这(按设计)性能提高了10倍,代表了光速数,因为这似乎是一种最佳实现方式。 因为我天真地写了我的Rust(和我的C仔细),我希望Rust的速度不会超过20% - 但我几乎可以支持任何东西。 或者至少,我以为我是; 实际上我真的对结果感到吃惊:

$ time ./statemap.rs/target/release/statemap ./pg-zfs.out> rs.svg
已处理3943472条记录,24999个矩形

真正的0m8.072s
用户0m7.828s
sys 0m0.186s
是的,你读得正确: 我天真的Rust比我精心实施的C快了约32% 。 这让我感到震惊,从那以后,我花了一些时间在一台运行SmartOS的真实实验室机器上(我已经复制了这些结果,并且能够对它们进行一些研究)。我的研究结果将不得不等待另一篇博客文章,但足以说尽管执行了一个令人震惊的相似数量的指令,但Rust实现有不同的加载/存储组合(它比C存储更重) - 并且相对于缓存表现得更好。鉴于Rust通过值的程度,这是有道理的,但更多的研究是值得的。

还值得一提的是,有一些简单的胜利可以让Rust实现更快:在我宣传了Rust实现状态图的事实之后,我很高兴Serde的作者之一David Tolnay花了时间提出一些改进的好建议。对于像我这样的新人来说,拥有如此深厚专业知识的人是一种很好的感觉,因为大卫有兴趣帮助我使我的软件表现得更好 - 而且它揭示了社区的核心价值观。

Rust令人震惊的良好表现 - 以及社区希望让它变得更好 - 从根本上改变了我对它的态度:不是将Rust视为增强 C语言和替换动态语言的语言,而是将其视为替代 C 语言的语言,以及除了堆栈的最低层之外的所有层中的动态语言。C - 就像汇编语言 - 将继续为我提供一个非常重要的地方,但是相对于Rust的活力四射的性能而言,很难看不到这个地方变得更小!

超越第一印象

我不想暗示这是一个详尽的清单,列出了我爱上Rust的一切。该清单要长得多,至少包括所有权模式; 特质体系; 货物; 类型推理系统。而且我觉得我刚刚划过了表面; 我没有深入了解FFI和并发模型等Rust的已知优势!(尽管在我的生活中编写了大量的多线程代码,但我没有在Rust中创建一个线程!)

建设未来

我可以充满信心地说,我的未来是在Rust。正如我在职业生涯中进行操作系统内核开发一样,一个自然的问题是:我是否打算在Rust中重写操作系统内核?总之,没有。为了理解我的不情愿,请参考我最近的一些经验:这篇博客文章被延迟了,因为我需要调试(并修复)我们实施Linux ABI的一个令人讨厌的问题。事实证明,Linux和SmartOS在vfork和信号的交互方面做出了略微不同的保证,而且我们的代码在一个应该是不可能的条件下致命地失败了。任何旧的Unix手(或快速研究!)都会告诉你,vfork和信号处理本身就是语义超级基金网站 - 而且它们的可怕(和不明确的)汇合只能是难以想象的毒性。但真正的问题是实际软件隐含地依赖于这些语义 - 任何想要运行现有软件的操作系统本身都必须模仿它们。您不想编写此代码,因为没有人想编写此代码。

现在,一个选项(我荣幸!)是从头开始重写操作系统,就好像遗留应用程序基本上不存在一样。虽然可以从中获得大量的好处(并且它可以找到许多用例),但它不适合我个人。

因此,虽然我可能不希望重写鲁斯特的操作系统内核,我确信铁锈一个很好的适合许多更广泛的系统中。例如,在最近的OpenZFS开发者峰会上,Matt Ahrens和我正在研究Rust中ZFS的用户级组件的概念。具体来说:zdb非常需要重写 - 而Rust会成为一个很好的候选者。在ZFS和整个系统中有很多这样的例子,包括内核中的一些。我们可能想要一个允许Rust驱动程序的设备驱动程序模型吗?也许! (当然,这在技术上是可行的。)在任何情况下,无论是在操作系统中,操作系统附近还是在操作系统之上,您都可以期望更多来自我的Rust,在无限期的未来。

行动吧

我写下这一切的部分内容不仅解释了我为什么要冒险,而且还鼓励其他人采取自己的方式。如果你像我一样,并正在考虑潜入Rust,有几条建议,无论它们值多少钱:

  • 我建议同时使用《Rust编程语言》和《编程Rust》。他们各自都很优秀,而且不同,足以让他们拥有两者。我还发现,在特别棘手的主题上有两个不同的来源是非常有价值的。

  • 在开始编写代码之前了解所有权。你越了解摘要中的所有权,你就越不需要在编译器错误消息的无情之手中学习。

  • 养成在短程序中运行rustc的习惯。 Cargo非常棒,但我个人发现编写简短的Rust程序来理解一个特定的想法是非常有价值的 - 特别是当你想要了解编译器的可选或新功能时。 (滚动,非词汇生活!)

  • 小心将一些东西移植到Rust作为第一个项目 - 或以其他方式实现之前实现的东西。现在,显然,这正是我所做的,能够将Rust中的实现与另一种语言的实现进行比较肯定是非常有价值的 - 但它也可能会对你产生影响:事实上我已经实现了状态图C向我发送了一些适合C的路径但对Rust来说是错误的;当我重新思考我的问题的执行方式时,我取得了更好的进展,就像Rust想让我思考它一样。

  • 退房的新Rustacean播客由克里斯Krycho。我非常喜欢克里斯的播客,并且在上下班或做家务时一直在努力。我特别喜欢他对Sean Griffen的采访以及他对Carol Nichols的采访。

  • 看看沙沙作响。我对此了解得太晚了; 我希望我早点知道它!我确实通过Rust koans工作,我很喜欢并且会在Rust的前几个小时推荐。

我确信我错过了很多东西; 如果您在学习Rust时发现有用的特定资源,请给我发消息或在此处留言,我会添加它。

最后,我要向Rust社区的工作人员表示衷心的感谢,他们长期致力于开发如此出色的软件 - 尤其是那些耐心地为我们的新手解释他们的工作的人。您应该为您所取得的成就感到自豪,无论是在革命性的技术还是热情的社区方面 - 感谢您激励我们这么多人了解基础设施软件可以成为什么样的东西,我期待将来用Rust编程很多年!