跳转至

Rust中的零成本抽象

原文地址

https://www.yuque.com/zmant/blog/rkyqbb

零成本抽象的概念对于某些编程语言是非常重要的,比如 Rust,C++,它们的目标就是让程序员用相对较少的精力开发出极具性能的程序。因为这个概念是 Rust 设计的基本原则,所以我想要研究一下零成本抽象到底是什么。

零成本抽象这个概念最初是由 C++ 的设计者 Bjarne Stroustrup 总结的:

What you don't use, you don't pay for. And further: What you do use, you could't hand code any better.

翻译成中文就是:

你不需要的东西,你不必付出代价。进一步说:你确实需要的东西,你无法手写出比这个更好的代码了。

根据这个定义,零成本抽象有两个关键因素:

  • 非全局成本:一个零成本抽象不应当降低不使用这个抽象的程序的性能。也就是说一个不使用这个抽象的程序没有理由为这个抽象付出代价。
  • 最佳性能:一个零成本抽象应该编译出程序员可以使用底层原语手写出的最佳实现。也就是说,若你确实需要这个抽象,那么你无法再写出比这个更好的代码了。

然而我认为零成本抽象还有一个很重要的因素。它通常容易被忽略,因为它只是所有优秀的抽象的要求,而不是零成本的要求:

  • 提升用户体验:抽象的要点就是通过封装较低级别的组件提供一个新的工具,以使得用户可以更容易的写出它们需要的程序。一个零成本的抽象也必须像其他的抽象一样,必须实际上提供比其他选择更好的体验。

所有这些因素都不可能完全全部实现,尤其是第三个,体验这种主观的东西是不可能完全让所有人都满意的。但是对于零成本抽象来说,第三点其实特别重要,因为第三点实际上和前面两点是相互对立的。一方面来说,它必须要比你手写代码有更好的性能。但另一方面,它也必须要比消耗性能成本以及其他非零成本抽象更好。这并不意味者我们必须严格的优于非零成本的抽象,因为性能只是其中一个因素,只要在可接受的范围内,即使花费一点额外的成本也是值得的。

(我认为某种程度上 Rust 试图通过让非零成本抽象使用起来更困难以使得让人感觉零成本抽象要更好一点。我认为这是一个错误,它损害了这门语言的整体用户体验,导致一些人都不去使用它,从而损害了我们的总体目标)

我还想说的是,实际上要实现一个同时满足上面三个要求的零成本抽象是非常困难的,也是非常了不起的。Rust 也仅仅只在少数几个地方完全做到了这一点(所有这些出色完成的地方都有极高的影响),比如 async/await 就做到了这一点。去年九月份的时候,我和我的一个朋友说,我担心我可能永远无法再把工作完成的像我刚刚完成的工作那样好(指 Pin API),我们很少能取地如此到的成就,因为它及其困难也需要很好的运气(很多问题或许仅仅只是缺少一个伟大的零成本抽象被发现)。

为了清晰表达我的观点,我列出一些在 Rust 中确实做的非常好的零成本抽象:

  • 所有权和借用,毫无疑问这是最突出的一个,在没有垃圾收集器的情况下保证了内存和线程安全,它是 Rust 最初获得巨大成功的关键。
  • 迭代器和闭包 APIs,这是另一个经典。某些情况下内部的迭代可能会做更好的优化,事实上,你可以在切片上直接些 map,filter,迭代循环并被优化到等价的手写的 C 代码。
  • async/await,futures,future API 是一个非常重要的例子,因为早期版本的 feture 只满足了零成本抽象的“零成本”部分,但实际上并提供足够好的体验来推动采用。通过添加 pin API 来支持 async/await、跨 await 的引用等等,我们已经开发了一个产品,我认为它将解决用户的问题,并使 Rust 在编写高性能网络服务时更加可行。
  • Unsafe 以及模块的边界,在所有这些抽象中,以及每个 Rust 成功的故事之后,都是不安全块和隐私的概念,这些概念允许我们利用原始指针操作来构建这些零成本抽象。如果没有这种真正的基本能力,即在本地打破规则、扩展超出 typechecker 处理能力的系统,就不可能实现 Rust 的优秀特性。这是零成本抽象,是 Rust 中所有其他零成本抽象的根本。

在其他方面,我们还没有找到像这样成功的零成本抽象。一个例子就是将 trait 对象作为动态分配多态性的解决方案(注意,动态分配是这个抽象的目标之一,因此通过虚函数调用的方式并不是零成本的)。

本文是对 withoutblogs 一篇博文的翻译,某些地方可能有根据自己的理解并没有完全按照原文翻译,如果想要查看原文的可以点击 Zero Cost Abstractions