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

标签 c++ memory optimization stack compiler-optimization

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

我期望编译器在 foo3 上创建 TCO,它首先销毁 sp 并通过不会创建堆栈的简单跳转调用 func框架。但它没有发生。程序在(汇编代码)第 47 行运行到 func,然后执行 call 并清除 sp 对象。即使我清除了 ~Simple(),优化也不会发生。

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

最佳答案

首先,请注意该示例有一个 double-free 错误。如果调用移动构造函数,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 采用非 const 右值引用。它可以修改参数。例如,它可能会离开它。但它可能不会。临时对象可能仍然拥有一个必须在调用后删除的缓冲区,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

关于c++ - C++ 编译器如何优化堆栈分配?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46996763/

相关文章:

c++ - Memcmp 似乎给我一个不正确的返回值

c - 为什么以这种方式传递结构会产生段错误?

python - SciPy 最小化 : How do I print the value of the finite difference Jacobian?

c# - 我在这里重复这个可枚举两次吗?

c++ - 删除被多个变量使用的指针

c++ - 在它所属的模板类中使用内部类

ios - MapBox 导致由于内存问题而终止

java - 为什么 foreach lambda 比其他 for 循环慢这么多?

c++ - Doxygen 不展开宏

c++ - 提取参数包