【问题标题】:How does C++ compiler optimize stack allocation?C++ 编译器如何优化堆栈分配?
【发布时间】:2018-04-10 08:11:39
【问题描述】:

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

我期待编译器在 foo3 上创建一个 TCO,它首先破坏 sp 并通过不会创建堆栈帧的简单跳转调用 func。但它没有发生。该程序在(汇编代码)第 47 行运行到 func,并在此之后使用 call 和干净的 sp 对象。即使我清除~Simple(),优化也不会发生。

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

【问题讨论】:

  • 伙计,您需要直接查看一些程序集,而不是查看打印语句。打印语句的存在可能会改变优化的方式。我推荐godbolt.org
  • 您不能像在例如print_mem 函数。那就是undefined behavior
  • @NirFriedman 感谢您的建议。我刚刚尝试过并更新了我的问题。
  • 如果您分享(godbolt.org 右侧的按钮)链接而不是图片,对每个人来说都会更容易

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


【解决方案1】:

首先,请注意该示例有一个双重释放错误。如果调用了移动构造函数,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* 的结构,我们已经涵盖了这一点。有趣的事实:我们不能在这里使用带有无操作删除器的std::unique_ptr,因为它不会被轻易破坏。

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

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

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。但是具有非平凡析构函数的类型不能在参数中传递。这意味着像 std::unique_ptr 这样的 RAII 类型不会让我们到达那里。使用 GSL 中的一个概念,我们至少可以表达所有权:

template<class T> using owner = T;
struct Simple {
  owner<int*> buffer = new int[1000];
};

那么我们可以希望现在或将来的静态分析工具能够检测到foo2正在接受所有权但永远不会删除buffer

【讨论】:

    猜你喜欢
    • 2011-03-15
    • 1970-01-01
    • 1970-01-01
    • 2014-12-31
    • 2016-10-25
    • 2016-07-07
    • 2014-02-21
    • 1970-01-01
    相关资源
    最近更新 更多