1

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

根据(强调我的)man页面:alloca

alloca() 函数在调用者的堆栈帧中分配 size 个字节的空间。 当调用 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 问题,所以让我清楚地重申我的目标:

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

1 回答 1

3

我正在编写一个必须始终在堆栈上分配其内存的容器(据我所知,这排除了 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 上,其中int32 位和指针是 64 位),并允许这种重载。

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

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

于 2018-03-18T03:28:10.230 回答