我找到了 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 没有。
更有趣的是buffer2
。 func
采用非 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/