在循环中初始化 struct/class 的效率损失

Efficiency penalty of initializing a struct/class within a loop

我已经尽力找到了这个问题的答案,但运气不佳。此外,我已经对其进行了测试,并且在优化的发布版本中没有发现任何差异(调试存在差异)......仍然,我无法想象为什么没有差异,或者优化器如何能够取消惩罚,也许有人知道内部发生了什么。

如果我在循环中创建简单 class/struct 的新实例,在每次循环迭代中创建 class/struct 是否会降低效率?

struct mystruct
{
    inline mystruct(const double &initial) : _myvalue(initial) {}
    double myvalue;
}

为什么...

for(int i=0; i<big_int; ++i)
{
    mystruct a = mystruct(1.1)
}

花费与

相同的实时时间
for(int i=0; i<big_int; ++i)
{
    double s = 1.1
}

?? constructor/initialization 不应该需要一些时间吗?

你的两个循环都没有做任何事情。死代码可能会被删除。此外,包含单个 doublestruct 和原始 double 之间没有表示差异。编译器应该能够轻松 "see through" 内联构造函数。 C++ 依赖于对这些东西的优化,以允许其抽象与手写版本竞争。

性能没有​​理由不同,如果是,我会认为这是一个错误(直到调试版本,调试信息可能会改变性能成本)。

C++ 哲学是你不应该 "pay"(在 CPU 周期或内存字节中)任何你不使用的东西。您的示例中的 struct 只不过是 double 与构造函数相关联。此外,可以内联构造函数,将开销降为零。

如果您的 struct 有其他部分需要初始化,例如其他字段或 table 虚函数,则会有一些开销。但是,按照您的示例设置方式,编译器可以优化构造函数,生成汇编输出,归结为 double.

的单个赋值

对于现代优化器来说,这是一项轻松的工作。

作为一名程序员,您可能会查看该构造函数和结构,并认为它必须付出一些代价。 "The constructor code involves branching, passing arguments through registers/stack, popping from the stack, etc. The struct is a user-defined type, it must add more data somewhere. There's aliasing/indirection overhead for the const reference, etc."

除了优化器随后会检查您的代码,它注意到 struct 没有虚函数,它没有需要非平凡构造函数的对象。整个事情都适合通用寄存器。然后它注意到您的构造函数只是将一个变量分配给另一个变量。它甚至可能会注意到你只是用一个文字常量调用它,它转换为一个单一的 move/store 指令到一个寄存器,它甚至不需要指令之外的任何额外内存。

这一切都非常神奇,编译器是复杂的野兽,但它们通常会多次执行此操作,从您的原始代码到中间表示,再从中间表示到机器代码。要真正欣赏和理解他们的工作,不时看一下拆解是值得的。

值得注意的是,C++ 已经存在了几十年。作为 C 的后继者,它最初主要被推为一种面向对象的语言,具有封装和信息隐藏等热门概念。为了推广一种人们开始替换 public 数据成员和手册 initialization/destruction 以及类似简单访问函数、构造函数、析构函数的语言,如果有一个可衡量的即使是简单的函数调用也会产生开销。听起来很神奇,C++ 优化器几十年来一直在这样做,压缩你添加的所有开销,使事情更容易维护到同一个程序集,而不是那么容易维护的东西。

因此,通常值得将函数调用和小型结构之类的事情视为基本上是免费的,因为如果值得内联并将所有开销压缩到零,优化器通常会这样做。间接函数调用会出现异常:虚拟方法、通过函数指针调用等。但是您发布的代码很容易被现代优化器压缩。

C++ 标准中的这些引用可能有助于理解允许的优化:

The semantic descriptions in this International Standard define a parameterized nondeterministic abstract machine. This International Standard places no requirement on the structure of conforming implementations. In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below.

还有:

The least requirements on a conforming implementation are:

  • Access to volatile objects are evaluated strictly according to the rules of the abstract machine.
  • At program termination, all data written into files shall be identical to one of the possible results that execution of the program according to the abstract semantics would have produced.
  • The input and output dynamics of interactive devices shall take place in such a fashion that prompting output is actually delivered before a program waits for input. What constitutes an interactive device is implementation-defined.

These collectively are referred to as the observable behavior of the program.

总而言之:编译器可以生成它喜欢的任何可执行文件,只要该可执行文件执行与未优化版本相同的 I/O 和对 volatile 变量的访问。特别是对时序和内存分配没有要求。


在您的代码示例中,整个事情都可以优化掉,因为它不会产生任何可观察到的行为。然而,现实世界的编译器有时会决定留下可以优化的东西,如果他们认为程序员出于某种原因真的希望这些操作发生的话。

@Ikes 的回答正是我的意思。但是,如果您对这个问题感到好奇,我强烈建议您阅读@dasblinkenlight、@Mankarse 和@Matt McNabb 的答案以及他们下面的讨论,其中包含有关情况的详细信息。谢谢大家