提问者:小点点

如何在C中模拟堆栈帧?


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

如果不知道这个容器的实现细节,当实例超出范围时,人们可能会期望它分配的任何内存都是空闲的。事实并非如此,并且可能导致堆栈溢出/在封闭函数期间内存使用率高。

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

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

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

我明白这可能是一个XY问题,所以让我明确重申我的目标:

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

共1个答案

匿名用户

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

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

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

C和C都没有指定alloca;它仅适用于具有像“普通”机器一样的堆栈的实现,即您可以期望VLA做您想做的事情的相同机器。

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

如果您有一个支持C99 VLA(如GNU C)的C编译器,智能编译器可以为具有循环范围的VLA重用相同的堆栈内存。

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

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

// 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位),并允许这种重载。

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

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