【问题标题】:Will the compiler optimize functions which return structures with fixed size arrays?编译器会优化返回具有固定大小数组的结构的函数吗?
【发布时间】:2016-09-08 04:17:55
【问题描述】:

假设我在 C/C++ 中有一个 struct 具有固定大小的数组成员,例如:

#define SIZE 10000
struct foo{
  int vector_i[SIZE];
  float vector_f[SIZE];
};

我想创建一个返回foo 实例的函数,例如:

foo func(int value_i, float value_f){
  int i;
  foo f;
  for(i=0;i<SIZE;i++) f.vector_i[i] = value_i;
  for(i=0;i<SIZE;i++) f.vector_f[i] = value_f;
  return f;
}

如果我使用以下方法调用函数:

foo ff = func(1,1.1);

编译器会执行某种优化(即 TCO)吗?

可执行文件会直接填充ff变量,还是先填充func中的f,然后将所有值从f复制到ff

如何检查优化是否执行?

【问题讨论】:

  • 这是一个相当大的对象,需要保存为局部变量。如果我计算正确,应该是 625kb(在 32 位平台上),在 windows 上我认为每个线程堆栈可以达到 1mb
  • 将指向结构的指针(或引用)作为参数传递,您根本不必担心。
  • 您可以查看生成的汇编程序。在 C++ 中,您还可以定义一个复制构造函数并查看它是否被调用。
  • 以下划线和大写字母开头的标识符被保留,在 C 中定义 _SIZE_ 将调用 未定义的行为
  • 如果您想查看已完成的优化,请编写一个最小的测试程序,然后从您的编译器获取汇编输出,并检查它以了解实际发生的情况。确实没有其他方法可以确定,优化不是标准化的(除了优化的代码必须表现得好像没有进行任何优化,除了未定义的行为,启用优化后事情会变得非常糟糕)。

标签: c++ c struct return-value return-value-optimization


【解决方案1】:

我的回答适用于 c++。

编译器会执行某种优化(即 TCO)吗?

您所说的 TCO 是指“尾调用优化”吗?该函数不会在最后进行函数调用(如果您愿意,可以进行尾调用),因此不适用优化。

编译器可以 elide 由于命名返回值优化,从返回值复制到临时。临时的复制初始化也可以省略。


如何检查优化是否执行?

通过阅读生成的汇编代码。

如果您看不懂汇编,另一种方法是添加具有副作用的复制和移动构造函数,并观察这些副作用是否发生。但是,修改程序可能会影响编译器是否决定优化(但不需要副作用来防止复制省略)。


如果您不想依赖优化,则应通过引用(c 中的指针)显式将现有对象传递给函数,并就地修改它。


复制省略的标准参考[class.copy] §31 (current standard draft)

当满足某些条件时,允许实现省略类对象的复制/移动构造,即使为复制/移动操作选择的构造函数和/或对象的析构函数有副作用。 [...]

该部分描述了在这种情况下满足的条件。该报价是根据 2016-04-07 的标准文档草案生成的。标准文件的不同版本的编号可能会有所不同,并且规则略有变化。引用的部分自 c++03 以来一直没有变化,其中该部分是 [class.copy] §15。

【讨论】:

  • 不调用这些副作用不会违反代码正确性吗?不确定 C++,但在 C 中,有一种叫做“可观察行为”的东西,它不能通过编译器优化而改变。
  • @Olaf 复制省略有一个特殊的例外来忽略 as-if 规则。我不知道c,所以我添加了一个免责声明(虽然,复制除了复制本身在c中也不会有副作用,可以吗?)。
  • @user2079303 在 C 中复制没有任何副作用,所以这不是问题。
  • 谢谢!您知道编译器是否将此优化视为标准(从 -O0 执行)还是包含在更高的优化值中?
  • @ztik 我见过 gcc 使用 -O0 应用复制省略
【解决方案2】:

这在Agner Fog's Calling Conventions document,§ 7.1 传递和返回对象,表 7. 返回结构、类和联合对象的方法中有很好的记录。

只有当它足够小并且不太复杂时,才能从寄存器中的函数返回结构、类或联合对象。如果对象太复杂或不适合适当的寄存器,则调用者必须为对象提供存储空间并将指向该空间的指针作为参数传递给函数。指针可以在寄存器或堆栈中传递。函数返回相同的指针。具体规则见表7。

换句话说,大返回对象直接在调用者提供的缓冲区中(在调用者的堆栈上)构造。

如果在编译时不知道要返回的对象的身份,则仍然需要额外的副本,例如:

foo func(bool a) {
    foo x, y;
    // fill x and y
    return a ? x : y; // copying is required here
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-25
    • 1970-01-01
    • 2016-06-18
    • 1970-01-01
    • 2019-01-11
    • 2017-07-04
    • 1970-01-01
    相关资源
    最近更新 更多