零开销原则有哪些重大例外(如果有的话)

What significant exceptions are there to the zero overhead principle, if any?

本文关键字:如果 原则 开销      更新时间:2023-10-16

例如,LLVM编码标准禁止使用标准RTTI或异常:http://llvm.org/docs/CodingStandards.html#do-不使用rtti或异常

这是一个好主意吗,还是编码标准对大多数程序来说已经过时或不合理?

C++中是否还有其他这样的功能,即使你不使用它们,也会显著降低程序的速度、内存使用率或可执行文件的大小?

这仍然是个好主意吗,还是编码标准已经过时了?

RTTI无疑是最臭名昭著的违反零开销原则的行为,因为它会产生静态成本(可执行文件大小和初始化代码),该成本与多态类(即至少有一个虚拟函数的类)的数量成正比,而且它不取决于你使用它的程度。但是,如果没有一些类开销,就无法真正提供RTTI。这就是为什么如果你根本不需要RTTI,或者如果你想用一个你有更多控制权的RTTI系统来取代它,你可以在大多数编译器上禁用RTTI(就像LLVM的人所做的那样)。然而,如果您启用了RTTI,并且没有使用它,则开销仅以代码膨胀(更大的可执行文件、所需的内存空间、更大的代码扩展)和加载/卸载时间的形式存在,因此,运行时执行开销几乎不存在。但是,在资源匮乏的环境中,或者对于小型实用程序(例如,在shell脚本中重复调用),静态开销可能太大,无法承受。此外,在没有太多实际情况下,您需要高性能RTTI,大多数时候您根本不需要它,而在其他情况下,在一些特殊的地方您需要它,与其他东西相比,这些地方通常对性能不重要。LLVM在这里是一个例外,因为编写编译器需要处理抽象语法树和类似的描述性类层次结构,如果没有大量的向下转换,这很难做到,而且由于分析这些结构是编译器的核心操作,因此向下转换(或其他RTTI调用)的性能至关重要。因此,不要把"不要使用RTTI"作为一条通则,只需知道它会带来什么开销,并知道它在成本/效益方面是否适合您的应用程序。

C++例外肯定是下一个可能会有比你准备讨价还价更多开销的事情。这是一个更有争议的问题,尤其是在实际描述异常总体开销时。凭经验评估异常的开销是一项非常困难的任务,因为它高度依赖于使用模式,即有不同的方法来使用异常,不同的严重程度(你是将异常用于错误、致命错误、异常情况,还是替换每个if语句?),并且对错误处理有不同级别的注意(无论是否使用异常)。当然,不同的编译器可以以不同的方式实现异常。当前的实现,即所谓的"零成本异常",旨在使正常执行期间的运行时成本为零,但这会留下相当多的静态开销,并使throw-to-catch执行路径变慢。这是一个很好的概述。至于违反"你只为你使用的东西付费"原则的例外情况,这是真的(除非你禁用了它们),但它们通常是合理的。异常的基本假设是,作为一名程序员,你打算编写健壮的错误处理代码,如果你正确地处理了所有错误,那么与错误处理代码(捕获块和析构函数)相比,异常的开销将相形见绌,您可能会有一个比具有相同错误处理量的等效C风格错误代码实现更小、更快的程序。但是,如果您不打算做太多的错误处理(例如,"如果出现任何问题,就崩溃!"方法),那么异常将产生大量开销。我不太清楚LLVM为什么禁止异常(如果必须直言不讳的话,我会说这是因为他们对错误处理并不认真,从我从该项目中看到的代码中可以看出)。所以,长话短说,指导方针应该是"如果你打算认真处理错误,就使用异常,否则就不要"。但请记住,这是一个激烈争论的话题。

是否有其他此类功能违反了"你只为你使用的东西付费"?

您已经命名为两个明显的功能,毫不奇怪,它们是大多数编译器都可以选择禁用的两个主要功能(在适当的时候通常会被禁用)。当然,还有其他更轻微的违反零开销原则的行为。

一个臭名昭著的例子是来自标准库的IO流库(<iostream>)。这个图书馆经常被批评为对大多数人的需求和使用有太多的开销。IO流库往往会引入大量代码,并且需要相当多的加载时初始化。然后,它的许多类(如std::ostreamstd::ofstream)在构造/销毁和读/写性能方面都有太多的运行时开销。我认为他们在该库中打包了太多的功能,而且由于大多数时候IO流任务都很简单,这些功能经常被闲置,其开销也不合理。但总的来说,我发现开销通常是可以接受的,尤其是因为大多数IO任务已经很慢了。顺便说一句,LLVM还禁止使用IO流库,这也是因为LLVM的目标是编写精简的命令行实用程序,这些实用程序可以执行大量文件IO(如编译器或其相关工具)。

可能还有其他标准库的开销比某些标准库在特定情况下所希望的要大。库代码经常不得不做出妥协,这种妥协不会让所有人都满意。我怀疑一些较新的库,如thread、chrono、regex和random,提供了比许多应用程序所需的更多的功能或健壮的保证,因此会带来一些不必要的开销。但话说回来,许多应用程序确实受益于这些功能。这就是妥协的意义所在。

至于语言规则会带来不必要的开销,有许多小问题会带来一些开销。首先,我可以想到标准必须做出保守假设以阻止优化的几个地方。一个值得注意的例子是无法限制指针别名,这迫使编译器假设任何内存都可以被任何指针别名(尽管在实践中,指针别名很少见),从而限制了优化的机会。在许多类似的情况下,编译器必须做出"安全"的假设,从而限制优化。但其中大多数在范围和潜在好处上都很小,而且它们通常是在能够保证行为的正确性(以及可重复性、健壮性、可移植性等)方面得到证明的。此外,请注意,在绝大多数情况下,它在其他语言中并没有真正变得更好,在C语言中可能会稍微好一点,但仅此而已。其中一些问题也可以通过编译器扩展或特定于平台的功能来解决,或者作为最后的手段,通过内联汇编代码来解决,也就是说,如果你真的需要优化到那个级别的话。

一个不再有效的例子是,即使对于永远不会抛出的函数,也需要编译器生成异常处理(堆栈展开)代码。现在,可以通过在所讨论的函数上指定noexcept来解决这个问题。

但除了这些微观问题之外,我真的想不出C++中过度开销的任何其他主要来源(除了RTTI和异常)。我的意思是,C++中有一些东西可以产生不同类型的开销,但它们都是"选择加入"功能(每次使用),如虚拟函数、虚拟继承、多重继承、模板等……但这些大多遵循"你只为你使用的东西付费"的原则。有关为C++的低开销子集强加的规则的示例,请查看Embedded C++。

相关文章: