C ++编译器如何优化堆栈分配?

时间:2017-10-29 04:07:31

标签: c++ memory optimization stack compiler-optimization

我找到了this post并写了一些像这样的测试:

我希望编译器在foo3上进行TCO,首先销毁sp并使用不会创建堆栈帧的简单跳转来调用func。但它没有发生。该程序在(汇编代码)第47行运行func,后面带有call和干净的sp对象。即使我清除~Simple(),也不会进行优化。

那么,在这种情况下如何触发TCO?

1 个答案:

答案 0 :(得分:1)

首先,请注意该示例具有双重免费错误。如果调用move-constructor,则sp.buffer未设置为nullptr,因此现在存在两个指向缓冲区的指针以便稍后删除。正确管理指针的更简单版本是:

struct Simple {
  std::unique_ptr<int[]> buffer {new int[1000]};
};

有了这个修复,让我们内联几乎所有内容,看看foo3真正做到的是什么:

using func_t = std::function<int(Sample&&)>&&;
int foo3(func_t func) {
  int* buffer1 = new int[1000]; // the unused local
  int* buffer2 = new int[1000]; // the call argument
  if (!func) {
    delete[] buffer2;
    delete[] buffer1;
    throw bad_function_call;
  }
  try {
    int retval = func(buffer2); // <-- the call
  } catch (...) {
    delete[] buffer2;
    delete[] buffer1;
    throw;
  }
  delete[] buffer2;
  delete[] buffer1;
  return retval;              // <-- the return
}

buffer1的情况很简单。它是一个未使用的本地,唯一的副作用是分配和释放,允许编译器跳过。足够智能的编译器可以完全删除未使用的本地。 clang ++ 5.0似乎可以实现这一目标,但g ++ 7.2却没有。

更有趣的是buffer2func采用非常量右值引用。它可以修改参数。例如,它可能会从中移动。但它可能不会。临时可能仍然拥有一个缓冲区,必须在调用后删除,foo3必须这样做。呼叫尾部呼叫。

如上所述,我们通过简单地泄漏缓冲区来接近尾调用:

struct Simple {
    int* buffer = new int[1000];
};

这有点作弊,因为问题的很大一部分是关于面对非平凡的析构函数的尾调用优化。但是让我们接受这个。如所观察到的,仅这一点不会导致尾调用。

首先,请注意,通过引用传递是一种通过指针传递的奇特形式。该对象仍然必须存在于某个地方,并且它位于调用者的堆栈中。需要在呼叫期间保持呼叫者的堆栈保持活动和非空状态将排除尾部呼叫优化。

要启用尾调用,我们希望在寄存器中传递func的参数,因此它不必存在于foo3的堆栈中。这表明我们应该通过价值:

int foo2(Simple); // etc.

SysV ABI规定要在寄存器中传递,它需要是可复制的,可移动的和可破坏的。作为一个包裹int*的结构,我们已经涵盖了这一点。有趣的事实:我们不能在{@ 1}}使用no-op删除器,因为这不是简单的可破坏的。

即便如此,我们仍然看不到尾调。我没有看到阻止它的原因,但我不是专家。用函数指针替换std::unique_ptr会导致尾调用。 std::function在调用中有一个额外的参数,并且有一个条件抛出。是否有可能使其难以优化?

无论如何,使用函数指针,g ++ 7.2和clang ++ 5.0进行尾调用:

std::function

但这是漏洞。我们可以做得更好吗?此类型具有所有权,我们希望将其从struct Simple { int* buffer = new int[1000]; }; int foo2(Simple sp) { return sp.buffer[std::rand()]; } using func_t = int (*)(Simple); int foo3(func_t func) { return func(Simple()); } 传递到foo3。但是具有非平凡析构函数的类型不能在参数中传递。这意味着像func这样的RAII类型不会让我们在那里。使用GSL的概念,我们至少可以表达所有权:

std::unique_ptr

然后我们可以希望现在或将来的静态分析工具可以检测到template<class T> using owner = T; struct Simple { owner<int*> buffer = new int[1000]; }; 正在接受所有权但从不删除foo2