查看原文
其他

程序中的“零成本抽象”如何实现?

withoutboats 高可用架构 2020-11-06

前言:在高可用架构后花园群的一次讨论中,有网友对 Rust 社区追捧的 async/await 不太理解,说起来这些特性在 .net 中十多年前就存在了。对于这一点,Rust 社区普遍的观点是, Rust 的 async/await 实现是零成本抽象,在无需增加额外全局成本,仅在设计层面就实现了对异步编程模型的支持,因此它跟之前别的语言的实现以及意义还是略有不同。考虑到“零成本抽象”这个概念不太好理解,并且程序员代码设计中也可以借鉴这些思路,所以高可用架构翻译了 Rust 核心开发工程师 withoutboats 这篇文章:零成本抽象。


零成本抽象的思想对于某些编程语言来说是非常重要的,比如 Rust 和 C++,它们的目的是让用户能够用相对较少的精力写出性能优异的程序。由于这个思想对于 Rust 的设计和我的工作来说是如此重要,所以我想通过本文探讨一下零成本抽象到底是什么。


这个想法是由 C++ 的早期开发者 Bjarne Stroustrup 总结出来的。


不用的东西,你不需要为之付出代价;再进一步说,用到的东西,你也不可能做得更好。
Bjarne Stroustrup

(为了避免翻译不够信雅达而影响原意,译者补充这句话英文原文如下:)


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


在这个定义中,有两个因素构成一个适当的零成本抽象。


  • 没有全局成本。零成本抽象不应该对不使用该功能的程序的性能产生负面影响。例如,它不能要求每个程序都携带一个沉重的语言运行时(runtime),以使唯一使用该功能的程序模块受益。

  • 最佳的性能。一个零成本的抽象应该编译成相当于底层指令编写的最佳实现。它不能引入额外的成本,而这些成本在没有抽象的情况下是可以避免的。


然而,我认为需要关注的是零成本的抽象还有第三个要求,这一点经常被忽略,因为它是所有好的抽象的一个需求,不管是不是零成本抽象。


  • 改善开发者的体验。抽象的意义在于提供一个新的工具,由底层组件组装而成,让开发者更容易写出他们想写的代码。零成本抽象,如同所有其他抽象一样,一定要比其他方法提供更好的使用体验。


对于零成本抽象来说,第三点其实特别重要,因为零成本抽象需要面对两个不同的竞争者。一方面,它一定比自己手写代码的在性能参数相同的情况下更有优势,这一点相当明确。但另一方面,它也一定比那些消耗性能以及使用非零成本的抽象要好。这并不意味着我们需要严格地比非零成本的抽象要好,因为性能成本是一个因素,但我们确实得把控好这个距离,使得即使承担额外的程序员开销,你都会觉得“零成本”是值得的。


(我认为 Rust 在某种程度上试图欺骗了这一点,让非零成本抽象的代码不太好实现,从而让人觉得零成本的抽象更好。我认为这是一个错误,它伤害了语言的整体用户体验,可能让一些人根本不愿意使用它,从而伤害了我们的整体目标)。


我还想说的是,真正创造一个零成本的抽象来实现这三点,是非常困难也是非常了不起的。Rust 只有几次真正出色地做到了这一点(都是极高的影响力),我参与了这些成功案例中的一个,那当然是 async/await,感觉就像手握火种一样。去年 9 月,我曾对一位朋友说,我担心自己以后永远也不可能做出这么漂亮的工作(指的是 Pin API),我确实有这种感觉。但我们很少有机会做出这么伟大的事情,因为它的难度极高,而且还涉及到相当多的运气成分(很多问题可能只是真正伟大的零成本抽象还没有被发现,至少在之前的设计约束下)。


为了更明确表达我的观点,我想列举几个 Rust 中真正伟大的零成本抽象。


  • 所有权和借用。当然这个是最大的一个,在没有垃圾回收器的情况下,保证内存和线程的安全,是 Rust 最初的最大的成功故事。

  • 迭代器和闭包 API。这又是一个经典之作。虽然有些情况下,内部迭代可能会优化得更好,但你可以在切片上写 map、filter、循环迭代等,它可以优化到相当于一些手写的 C 语言,这绝对是令人震惊的。

  • Async/await 和 Future。Futures API 是一个重要的例子,因为早期版本的 futures 在零成本抽象的“零成本”部分做得非常好,但实际上并没有提供足够好的用户体验来吸引用户采用。通过增加 pining 支持 async/await、跨 await 的引用等等,我们做了一个真的可以解决用户的问题的产品,让 Rust 更适合编写高性能的网络服务。

  • Unsafe 和模块边界。在所有这些,以及 Rust 的每一个成功案例的背后,都是不安全块和隐私的概念,让我们可以侵入到原始指针操作中去构建这些零成本的抽象。如果没有这种真正根本性的能力,任何一个 Rust 的辉煌成就都是不可能实现的,因为我们可以在本地环境打破规则,将系统扩展到类型检查器所能处理的范围之外。这就是零成本抽象,它是 Rust 中所有其他零成本抽象的基础。


在其他领域,我们还没有那么成功地找到零成本抽象。这方面的一个例子就是将 trait 对象作为动态调度多态的解决方案。(这里要注意,这里的动态调度是需求的一部分,所以虚拟调用不是非零成本的)。问题是对象的安全,以及有大小的类型(sized type)和非大小类型(unsized type)概念,类型转换的不好的语法,使得 trait 对象在工作中真的很不方便;当我不得不使用它们时,我通常会很苦恼。至少在过去的 18 个月里,我一直想真正挖掘这个问题空间,但其他的事情总是占据了我的优先级。


英文原文:

https://boats.gitlab.io/blog/post/zero-cost-abstractions/


参考阅读:

本文由 Tim 翻译技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

高可用架构
改变互联网的构建方式

长按二维码 关注「高可用架构」公众号

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存