我正在编写一个在内部使用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,所以让我清楚地重申我的目标:
std::unique_ptr
保存的。答案 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位),并允许这种重载。
容量增长到足以需要动态分配的容器应继续使用该空间,但随后缩小应继续使用动态空间,因此再次增长更便宜,并避免复制回内部存储。除非调用者使用函数释放未使用的多余存储空间,否则请将其复制回来。
移动构造函数应该按原样保持分配,但是如果可能的话,复制构造函数应该复制到内部缓冲区而不是分配新的动态存储。