零成本抽象所需的最小优化集是什么?

2020-08-09 11:55:04

Rust和C++的一个引人注目的特性是零成本抽象。您可以编写高级代码,例如使用迭代器,这些代码会编译成与您手动编写的低级代码相同的机器代码。您可以添加抽象层,例如,将原始值包装在结构中并为其提供专门的API,而不会增加运行时开销。但是,仅当您启用了一组足够的编译器优化时,零成本才适用。不幸的是,启用这些优化会减慢编译速度,并且使用当前的编译器会浪费大量调试信息,使得使用这些二进制文件进行调试变得非常困难。由于Rust标准库(以及越来越多的C++标准库)大量使用零成本抽象,因此为了更好的调试和构建时间,使用非优化构建会创建比发布构建慢很多倍的二进制文件,而且通常非常慢。所以问题是:我们如何在保持零成本抽象的同时获得快速构建和高质量的调试信息?一种明显的方法是将启用的编译器优化集限制在实现零成本抽象所需的最低限度,并希望这能产生可接受的构建速度和调试信息质量。但是那是什么布景呢?如果它是所有的优化,那么这是没有帮助的。我猜这取决于我们想要支持的确切的零成本抽象模式集。以下是我认为需要列入列表的一些优化:内联:大多数或所有抽象都依赖于内联函数来实现零成本,因为它们将代码封装到您自己编写的函数中。所以,我们必须有侵略性的内衬。复制传播:内联函数之后,通过参数和结果的赋值链将需要使用复制传播来缩短。有限的死区消除和临时消除:复制传播后,不需要大量临时值。我们不应该存储它们的值或为它们分配空间。标量替换:许多抽象将变量打包到结构中,并将结构上的操作封装到函数中。优化器似乎需要能够撤销这一点,拆分结构并像对待任何其他标量变量一样对待原始组件。要使上面的优化在这些组件上工作,通常需要这样做。

下面是一些我认为不需要出现在列表中的优化:寄存器分配:未经优化的调试构建通常几乎不进行寄存器分配:局部变量位于堆栈插槽中。因此,如果消除了不必要的临时变量(见上文),则将幸存的变量存储在堆栈上并不是对抽象的惩罚。矢量化:未经优化的调试版本通常不做任何矢量化,所以我们可以继续不做,而不惩罚抽象。尾部调用:我想不出有哪种Rust或C++抽象依赖于TCO是零成本的。

这是一个我不太确定的优化:公共子表达式消除:在Rust和C++中,有没有我们认为应该是零成本的需要CSE的通用抽象?我脑子里想不出任何东西。

深入研究这一点并使用一组有前途的优化来实际配置Rust编译器并评估结果将是非常有趣的。解决整个问题的一种有点正交的方法是简单地提高优化构建的调试信息质量和构建速度。前者主要是大量的工程和架构工作,以便通过各种优化过程保存调试信息。有些问题是,某些优化(例如寄存器分配)会导致变量值在技术上仍在作用域内的程序点不可用,但像RR或更好的像Pernosco这样更好的记录和重放调试器可以绕过这一问题。构建速度确实是一个更难的问题:进行大量优化,特别是在所有簿记以保留调试信息的情况下,这本身就很昂贵。我想知道,如果您从零成本抽象的同时,以良好的调试信息和尽可能快的构建为目标,从头开始设计编译器后端会是什么样子。