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

时间:2018-03-18 01:56:55

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

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

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

  

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超出范围时,可能会期望它分配的任何内存都是空闲的。情况并非如此,并且在封闭功能的持续时间内可能导致堆栈溢出/高内存使用。

我想到的一种方法是在析构函数中明确释放内存。如果没有对生成的程序集进行逆向工程,我还没有找到一种方法(参见this)。

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

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

我很欣赏这可能是XY problem,所以让我清楚地重申我的目标:

  • 我正在编写一个必须总是在堆栈上分配内存的容器(据我所知,这排除了C VLA)。
  • 在编译时不知道容器的大小。
  • 我想维护内存的语义,好像它是由容器内的std::unique_ptr保存的。
  • 虽然容器必须具有C ++ API,但使用C语言的编译器扩展是可以的。
  • 此代码目前仅适用于x86_64。
  • 目标操作系统可以是基于Linux的,也可以是Windows,不需要同时使用它们。

1 个答案:

答案 0 :(得分:3)

  

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

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

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

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

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

如果您的C ++编译器支持C99 VLA(如GNU 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位),并允许这种重载。

容量增长到足以需要动态分配的容器应继续使用该空间,但随后缩小应继续使用动态空间,因此再次增长更便宜,并避免复制回内部存储。除非调用者使用函数释放未使用的多余存储空间,否则请将其复制回来。

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