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

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

我正在编写一个容器,它在内部使用 alloca 在堆栈上分配数据。 Risks of using alloca aside,假设我必须将它用于我所在的域(这部分是围绕 alloca 的学习练习,部分是为了研究动态大小的堆栈分配容器的可能实现)。

根据 man page for alloca(强调我的):

The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automatically freed when the function that called alloca() returns to its caller.

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

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

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 超出范围时被释放。情况并非如此,并且可能导致封闭函数持续时间内的堆栈溢出/高内存使用率。

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

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

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

我很感激这可能是一个 XY problem,所以让我重申一下我的目标:

I am writing a container that must always allocate its memory on the stack (to the best of my knowledge, this rules out C VLAs).

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

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

C 和 C++ 均未指定 alloca;它仅适用于具有像 "normal" 机器这样的堆栈的实现,即您可以期望 VLA 执行您想要的操作的相同机器。

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


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


have a maximum size specified at compile-time, use that to allocate a fixed-size buffer ... wasteful

对于您提到的特殊情况,您可能 将 fixed-size 缓冲区作为对象的一部分(大小作为模板参数),然后使用它如果它足够大。如果没有,动态分配。也许使用一个指针成员指向内部或外部缓冲区,并使用一个标志来记住是否在析构函数中 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 只需要在增长时进行检查,所以我将其设为 signed int 以便我们可以重载它而不需要额外的布尔成员。

通常一个容器使用 3 个指针而不是指针 + 2 个整数,但如果你不经常 grow/shrink 那么我们保存 space(在 x86-64 上 int 是32 位和指针是 64 位),并允许这种重载。

增长到需要动态分配的容器应该继续使用那个 space 但收缩后应该继续使用动态 space,这样再次增长会更便宜,并避免复制回来进入内部存储。除非调用者使用函数释放未使用的多余存储,然后复制回来。

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