1

如果我有一个没有状态的仿函数类,但是我使用 new 从堆中创建它,那么典型的编译器是否足够聪明,可以完全优化创建开销?

这个问题是在制作一堆无状态函子时出现的。如果它们被分配在堆栈上,它们的 0 状态类主体是否意味着堆栈真的根本没有改变?看来它必须以防您稍后获取仿函数实例的地址。堆分配也一样。

在这种情况下,仿函数总是在它们的创建中增加一个(微不足道的,但非零的)开销。但也许编译器可以看到该地址是否被使用,如果没有,它可以消除堆栈分配。(或者,它甚至可以消除分配吗?)

但是作为临时创建的函子怎么样?

#include <iostream>

struct GTfunctor 
{
  inline bool operator()(int a, int b) {return a>b; }
};

int main()
{
  GTfunctor* f= new GTfunctor;
  GTfunctor g;

  std::cout<< (*f)(2,1) << std::endl;
  std::cout<< g(2,1) << std::endl;
  std::cout<< GTfunctor()(2,1) << std::endl;
  delete f;
}

所以在上面的具体例子中,三行分别以三种不同的方式调用同一个函子。在此示例中,这些方式之间是否存在效率差异?或者编译器是否能够一直优化每一行,直到成为一个无计算的打印语句?

编辑:大多数答案说编译器永远不能内联/消除堆分配的仿函数。但这也是真的吗?大多数编译器(GCC、MS、Intel)也有链接时间优化,它们确实可以进行这种优化。(但有吗?)

4

9 回答 9

3

显然,这取决于您的编译器。

我会说

  • 没有编译器会优化堆上的对象。(这是因为,正如 ChrisW 所说,编译器永远不会优化对 new 的调用,这几乎可以肯定是在另一个翻译单元中定义的。)

  • 一些编译器会优化掉堆栈上的命名对象。我知道 gcc 经常做这种优化。

  • 大多数编译器会优化掉堆栈上的未命名对象。这是“标准”C++ 优化之一,尤其是当更高级的 C++ 用户倾向于创建大量未命名的临时变量时。

不幸的是,这些只是经验法则。优化器是出了名的不可预测。真正了解编译器在做什么的唯一方法是读取汇编输出。

于 2009-05-11T02:46:52.377 回答
3

典型的编译器是否足够聪明,可以完全优化创建开销?

当您在堆上创建它们时,我怀疑是否允许编译器这样做。海事组织:

  • 调用 new 意味着调用 operator new。
  • operator new 是在运行时库中定义的重要函数。
  • 不允许编译器决定您并不是真的要调用这样的函数,并决定作为优化它不会默默地调用它。

当您在堆栈上创建它们并且不获取它们的地址时,那么可能......或者可能不是:我的猜测是每个对象都有一个非零大小,以便占用一些内存,以便拥有一个身份,即使对象除了其身份之外没有任何状态。

于 2009-05-11T02:07:54.640 回答
1

我非常怀疑这种优化是允许的,但是如果你的仿函数没有状态,你为什么要在堆上初始化它呢?它应该像临时使用一样容易。

于 2009-05-11T02:31:06.053 回答
1

C++ 对象的大小始终非零。“空基类优化”允许空基类的大小为零,但这不适用于此处。

我没有研究过任何 C++ 优化器,所以我所说的只是推测。我认为 2nd 和 3rd 将很容易内联扩展,不会有任何开销,也不会创建 GTFunctor。然而,函子指针是另一回事。在您的示例中,它可能看起来很简单,并且任何优化器都应该能够消除堆分配,但是在一个不平凡的程序中,您可能会在一个翻译单元中创建函子并在另一个翻译单元中使用它。甚至在编译器/链接器/加载器/运行时系统没有源代码的不同库中,几乎不可能优化。考虑到优化它并不容易,性能的潜在收益并不大,并且在堆中分配空函子的情况可能很少,

于 2009-05-11T02:38:09.647 回答
1

编译器无法优化对 new 或 delete 的调用。但是,它可能会优化在堆栈上创建的变量,因为它没有状态。

于 2009-05-11T02:38:41.487 回答
1

回答堆问题的简单方法:

GTfunctor *f = new GTfunctor;

的值f不能为空,那应该是什么呢?你还有:

GTfunctor *g = new GTfunctor;

现在 的值g不能等于 的值f,那么每个应该是什么?

此外,也不fg可能不等于从 获得的任何其他指针new,除非其他地方的某个指针以某种方式初始化为等于for g,这(取决于后面的代码)可能涉及检查程序的整个其余部分所做的事情。

是的,如果通过对代码的本地检查,编译器可以看到您从不依赖这些要求中的任何一个,那么它可以执行重写,这样就不会发生堆分配。问题是,如果您的代码如此简单,您可能会自己重写并最终得到一个更具可读性的程序,例如,您的测试程序看起来像您的基于堆栈的g示例。所以真正的程序不会从编译器中的这种优化中受益。

大概你这样做的原因是因为有时仿函数确实有数据,这取决于在运行时选择的类型。所以编译时分析在这里不能有效地发挥它的魔力。

于 2009-05-11T11:08:26.223 回答
1

C++ 标准规定每个对象(堆上的恕我直言)必须至少有一个字节的大小,因此它可以被唯一地寻址。

使用 new 生成函子会导致两个问题:

  1. 结构通常不能优化掉。New 是一个具有复杂副作用的函数 (bad_alloc)。
  2. 因为您间接寻址仿函数,编译器可能无法内联该函数。

如果您在堆栈上生成函子,您很可能看不到函子的迹象。

旁注:内联语句不是必需的。在类定义中定义的每个函数都被视为可内联的。

于 2009-05-11T22:08:47.403 回答
0

你的问题的答案有两个方面。

  1. 编译器是否优化了堆分配:我强烈怀疑它,但我不是一个标准的人,所以我必须查一下。

  2. 编译器可以通过内联对象的 operator() 进行优化吗?是的。只要您不将调用指定为虚拟调用,即使指针取消引用也不会实际执行。

于 2009-05-11T10:09:34.837 回答
0

编译器可能会发现 operator() 没有使用任何成员变量,并将其优化到最大值。不过,我不会对本地或堆分配的变量做出任何假设。

编辑:如有疑问,请打开编译器上的程序集输出选项,看看它实际在做什么。当您可以自己看到真正的答案时,在网络上听一群白痴是没有意义的。

于 2009-05-11T02:33:00.323 回答