如何在C++中模拟堆栈帧

How can I emulate a stack frame in C++?

本文关键字:模拟 堆栈 C++      更新时间:2023-10-16

我正在编写一个容器,该容器在内部使用alloca来分配堆栈上的数据。撇开使用alloca的风险不谈,假设我必须将其用于我所在的域(这部分是围绕alloca的学习练习,部分是为了研究动态大小的堆栈分配容器的可能实现)。

根据allocaman页面(强调我的):

alloca() 函数在调用方的堆栈帧中分配大小字节的空间。当调用 alloca() 的函数返回给其调用方时,会自动释放此临时空间。

使用特定于实现的功能,我设法以这样的方式强制内联,即调用者堆栈用于此函数级"范围"。

但是,这意味着以下代码将在堆栈上分配大量内存(编译器优化除外):

for(auto iteration : range(0, 10000)) {
// the ctor parameter is the number of
// instances of T to allocate on the stack,
// it's not normally known at compile-time
my_container<T> instance(32);
}

如果不知道此容器的实现细节,则可能会期望它分配的任何内存在超出范围时instance释放。事实并非如此,可能会导致在封闭函数期间出现堆栈溢出/高内存使用率。

我想到的一种方法是显式释放析构函数中的内存。除了对生成的程序集进行逆向工程之外,我还没有找到一种方法(另请参阅此内容)。

我想到的唯一另一种方法是在编译时指定最大大小,使用它来分配固定大小的缓冲区,在运行时指定实际大小并在内部使用固定大小的缓冲区。这样做的问题是它可能非常浪费(假设每个容器的最大字节为 256 字节,但大多数时候您只需要 32 个字节)。

因此,这个问题;我想找到一种方法来向此容器的用户提供这些范围语义。不可移植是可以的,只要它在平台上是可靠的,它的目标(例如,一些只适用于x86_64的文档编译器扩展就可以了)。

我知道这可能是一个XY问题,所以让我清楚地重申我的目标:

  • 正在编写一个容器,该容器必须始终在堆栈上分配其内存(据我所知,这排除了 C VLA)。
  • 容器的大小在编译时未知。
  • 我想保持内存的语义,就好像它是由容器内的std::unique_ptr持有一样。
  • 虽然容器必须具有C++ API,但使用 C 编译器扩展是可以的。
  • 代码现在只需要在x86_64上工作。
  • 目标操作系统可以基于 Linux 或 Windows,它不需要同时在两者上运行。

我正在编写一个容器,该容器必须始终在堆栈上分配其内存(据我所知,这排除了 C VLA)。

大多数编译器中 C VLA 的正常实现都在堆栈上。 当然,ISO C++并没有说明如何在后台实现自动存储,但它(几乎?)对于普通机器(确实有调用+数据堆栈)上的C实现来说是通用的,可以将其用于所有自动存储,包括VLA。

如果您的VLA太大,您将获得堆栈溢出,而不是回退到malloc/free

C 和 C++ 都没有指定alloca;它只适用于具有像"普通"机器这样的堆栈的实现,即您可以期望 VLA 做你想做的事的相同机器。

所有这些条件都适用于 x86-64 上的所有主要编译器(MSVC 不支持 VLA 除外)。


如果您有支持 C99 VLA(如 GNU C++)的C++编译器,智能编译器可能会对具有循环作用域的 VLA 重用相同的堆栈内存。


在编译时指定最大大小,使用它来分配固定大小的缓冲区...浪费

对于您提到的特殊情况,您可以将固定大小的缓冲区作为对象的一部分(大小作为模板参数),如果它足够大,则可以使用它。 如果没有,请动态分配。 可以使用指针成员指向内部或外部缓冲区,并使用一个标志来记住是否在析构函数中delete它。 (当然,您需要避免在作为对象一部分的数组上delete

// optionally static_assert (! (internalsize & (internalsize-1), "internalsize not a power of 2")
// if you do anything that's easier with a power of 2 size
template <type T, size_t internalsize>
class my_container {
T *data;
T internaldata[internalsize];
unsigned used_size;
int allocated_size;   // intended for small containers: use int instead of size_t
// bool needs_delete;     // negative allocated size means internal
}

allocated_size只需要在它增长时进行检查,所以我让它签名为 int,这样我们就可以重载它,而不是需要一个额外的布尔成员。

通常容器使用 3 个指针而不是指针 + 2 个整数,但如果您不经常增长/收缩,那么我们会节省空间(在 x86-64 上,int为 32 位,指针为 64 位),并允许此重载。

增长到需要动态分配的容器应继续使用该空间,但随后收缩应继续使用该动态空间,因此再次增长的成本更低,并避免复制回内部存储。 除非调用方使用函数释放未使用的多余存储空间,否则请复制回来。

移动构造函数可能应保持分配不变,但复制构造函数应尽可能复制到内部缓冲区中,而不是分配新的动态存储。