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

标签 c++ x86-64 alloca stack-allocation

我正在编写一个容器,它在内部使用 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.

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

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

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 , 所以让我清楚地重申我的目标:

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

最佳答案

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 实现来说是通用的将其用于所有自动存储,包括 VLA。

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

C 和 C++ 都没有指定 alloca;它仅适用于具有类似于“普通”机器的堆栈的实现,即您可以期望 VLA 执行您想要的操作的相同机器。

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


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


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

对于您提到的特殊情况,您可以将固定大小的缓冲区作为对象的一部分(大小作为模板参数),如果它足够大就使用它。如果没有,动态分配。也许使用一个指针成员指向内部或外部缓冲区,并使用一个标志来记住是否在析构函数中删除它。 (当然,您需要避免对作为对象一部分的数组执行 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 以便我们可以重载它而不需要额外的 bool 成员。

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

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

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

关于c++ - 如何在 C++ 中模拟堆栈帧?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49343804/

相关文章:

c - 如何使用alloca分配C函数指针?

c++ - Qt::BackgroundRole 似乎被忽略了

c++ - 从服务打开 UWP 应用程序

c++ - 用于单个生产者单个消费者 FIFO 的 STL 容器?

c++ - 在 gtk 中绘制点/线。 C++

linux - 通过 linux x86-64 函数调用保留哪些寄存器

C 函数调用的自定义 X86_64 调用约定

c++ - 如何在内联汇编中实现这个?

c++ - 在 C++ 中调整动态堆栈分配的大小

visual-c++ - 如何在没有 _alloca 的情况下进行 GCC 编译?