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

C++ C++;编译器优化堆栈分配?,c++,memory,optimization,stack,compiler-optimization,C++,Memory,Optimization,Stack,Compiler Optimization,我发现并编写了如下测试: 我希望编译器在foo3上生成一个TCO,首先销毁sp,然后通过一个简单的跳转调用func,而不会创建堆栈帧。但事实并非如此。程序运行到(汇编代码)第47行的func,之后有call和cleansp对象。即使我清除了~Simple(),优化也不会发生 那么,在这种情况下,如何触发TCO呢?首先,请注意,该示例有一个双自由bug。如果调用了move构造函数,sp.buffer不会像必须的那样设置为nullptr,因此现在存在两个指向缓冲区的指针,以便稍后删除。正确管理指针

我发现并编写了如下测试:

我希望编译器在
foo3
上生成一个TCO,首先销毁
sp
,然后通过一个简单的跳转调用
func
,而不会创建堆栈帧。但事实并非如此。程序运行到(汇编代码)第47行的
func
,之后有
call
和clean
sp
对象。即使我清除了
~Simple()
,优化也不会发生


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

首先,请注意,该示例有一个双自由bug。如果调用了move构造函数,
sp.buffer
不会像必须的那样设置为
nullptr
,因此现在存在两个指向缓冲区的指针,以便稍后删除。正确管理指针的更简单版本是:

struct Simple {
  std::unique_ptr<int[]> buffer {new int[1000]};
};
buffer1
的情况很简单。它是一个未使用的本地文件,唯一的副作用是分配和解除分配,编译器可以跳过这些操作。一个足够智能的编译器可以完全删除未使用的本地文件。clang++5.0似乎可以实现这一点,但g++7.2却不能

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

正如所观察到的,我们只需泄漏缓冲区,就更接近尾部调用:

struct Simple {
    int* buffer = new int[1000];
};
这有点作弊,因为问题的很大一部分是关于在非平凡析构函数面前的尾部调用优化。但让我们娱乐一下。正如所观察到的,这本身并不会导致尾部调用

首先,请注意,按引用传递是按指针传递的一种奇特形式。对象仍然必须存在于某个地方,并且该位置在调用方的堆栈上。在调用过程中需要保持调用方堆栈的活动性和非空性,这将排除尾部调用优化

要启用尾部调用,我们需要在寄存器中传递
func
的参数,这样它就不必存在于
foo3
的堆栈中。这表明我们应该按价值传递:

int foo2(Simple); // etc.
SysV ABI规定,要在寄存器中传递,它必须是可复制、可移动和可破坏的。作为一个包装
int*
的结构,我们已经介绍了这一点。有趣的事实:我们不能将std::unique_ptr与no-op-deleter一起使用,因为这不是简单的可破坏性

即便如此,我们仍然没有看到尾声。我看不出有什么理由阻止它,但我不是专家。用函数指针替换
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\u ptr
这样的RAII类型将无法实现这一目标。使用GSL的概念,我们至少可以表达所有权:

template<class T> using owner = T;
struct Simple {
  owner<int*> buffer = new int[1000];
};
使用owner=T的模板;
结构简单{
所有者缓冲区=新整数[1000];
};

然后我们可以希望现在或将来的静态分析工具能够检测到
foo2
正在接受所有权,但从不删除
buffer

Dude,您需要直接查看某些程序集,而不是查看print语句。print语句的出现可能会改变优化的方式。我建议。您不能像在
print\u mem
函数中那样可靠地对两个不相关的指针执行算术运算。弗里德曼:谢谢你的建议。我刚刚试过并更新了我的问题。如果你能分享(godbolt.org右侧的按钮)链接而不是图片,对每个人来说都会更容易
template<class T> using owner = T;
struct Simple {
  owner<int*> buffer = new int[1000];
};