关于alloca的使用和滥用
Posted
技术标签:
【中文标题】关于alloca的使用和滥用【英文标题】:On the use and abuse of alloca 【发布时间】:2011-08-14 00:53:49 【问题描述】:我正在开发一个软实时事件处理系统。我想尽量减少我的代码中具有不确定时间的调用。我需要构建一个由字符串、数字、时间戳和 GUID 组成的消息。可能是boost::variant
的std::vector
。
我一直想在过去类似性质的代码中使用alloca
。然而,当人们查看系统编程文献时,总是对这个函数调用非常谨慎。就我个人而言,在过去的 15 年中,我无法想到没有虚拟内存的服务器类机器,而且我知道 Windows 堆栈一次增长一个虚拟内存页面,所以我假设Unices也一样。这里(不再)没有砖墙,堆栈和堆一样可能耗尽空间,所以什么给出了?为什么人们不喜欢 aloca 呢?我可以想到许多负责任地使用 alloca 的用例(字符串处理任何人?)。
无论如何,我决定测试性能差异(见下文),alloca 和 malloc 之间存在 5 倍的速度差异(测试捕获了我将如何使用 alloca)。那么,事情发生了变化吗?只要我们可以绝对确定我们的对象的生命周期,我们是否应该放弃谨慎并使用alloca
(包裹在std::allocator
)?
我厌倦了生活在恐惧中!
编辑:
好的,所以有限制,对于 Windows,这是一个链接时间限制。对于 Unix,它似乎是可调的。似乎页面对齐的内存分配器是有序的:D 任何人都知道通用的可移植实现:D?
代码:
#include <stdlib.h>
#include <time.h>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <iostream>
using namespace boost::posix_time;
int random_string_size()
return ( (rand() % 1023) +1 );
int random_vector_size()
return ( (rand() % 31) +1);
void alloca_test()
int vec_sz = random_vector_size();
void ** vec = (void **) alloca(vec_sz * sizeof(void *));
for(int i = 0 ; i < vec_sz ; i++)
vec[i] = alloca(random_string_size());
void malloc_test()
int vec_sz = random_vector_size();
void ** vec = (void **) malloc(vec_sz * sizeof(void *));
for(int i = 0 ; i < vec_sz ; i++)
vec[i] = malloc(random_string_size());
for(int i = 0 ; i < vec_sz ; i++)
free(vec[i]);
free(vec);
int main()
srand( time(NULL) );
ptime now;
ptime after;
int test_repeat = 100;
int times = 100000;
time_duration alloc_total;
for(int ii=0; ii < test_repeat; ++ii)
now = microsec_clock::local_time();
for(int i =0 ; i < times ; ++i)
alloca_test();
after = microsec_clock::local_time();
alloc_total += after -now;
std::cout << "alloca_time: " << alloc_total/test_repeat << std::endl;
time_duration malloc_total;
for(int ii=0; ii < test_repeat; ++ii)
now = microsec_clock::local_time();
for(int i =0 ; i < times ; ++i)
malloc_test();
after = microsec_clock::local_time();
malloc_total += after-now;
std::cout << "malloc_time: " << malloc_total/test_repeat << std::endl;
输出:
hassan@hassan-desktop:~/test$ ./a.out
alloca_time: 00:00:00.056302
malloc_time: 00:00:00.260059
hassan@hassan-desktop:~/test$ ./a.out
alloca_time: 00:00:00.056229
malloc_time: 00:00:00.256374
hassan@hassan-desktop:~/test$ ./a.out
alloca_time: 00:00:00.056119
malloc_time: 00:00:00.265731
--编辑:家用机器、clang 和 google perftools 上的结果--
G++ without any optimization flags
alloca_time: 00:00:00.025785
malloc_time: 00:00:00.106345
G++ -O3
alloca_time: 00:00:00.021838
cmalloc_time: 00:00:00.111039
Clang no flags
alloca_time: 00:00:00.025503
malloc_time: 00:00:00.104551
Clang -O3 (alloca become magically faster)
alloca_time: 00:00:00.013028
malloc_time: 00:00:00.101729
g++ -O3 perftools
alloca_time: 00:00:00.021137
malloc_time: 00:00:00.043913
clang++ -O3 perftools (The sweet spot)
alloca_time: 00:00:00.013969
malloc_time: 00:00:00.044468
【问题讨论】:
看来,启用优化的 clang 测试是错误的。对 alloca 的内部调用被优化(删除),因为没有副作用(在 llvm IR 代码级别)或使用它们的结果。 我与那些曾经构建商品嵌入式系统中使用的硬件的人 (EE) 一起工作,例如有线电视公司的网关。他们分配了一个固定大小的缓冲区,然后重新使用它。从未进入内存管理器。 【参考方案1】:首先,即使有很多虚拟内存并不意味着您的进程将被允许填充它。在 *nix 上有堆栈大小限制,而堆则更宽松。
如果您只打算分配几百/几千个字节,请务必继续。除此之外的任何事情都将取决于任何给定系统上的限制 (ulimit),而这只是灾难的根源。
Why is the use of alloca() not considered good practice?
在我工作的开发箱 (Gentoo) 上,我的默认堆栈大小限制为 8192 kb。这不是很大,如果 alloca 溢出堆栈,则行为未定义。
【讨论】:
我想知道,你知道在 Unix 上是否有某种受控的退出信号,用于堆栈溢出? 快速man 7 signal
不会显示任何内容。据我所知,严重的堆栈溢出将导致 SIGSEGV。 IIRC 你可以抓到一个 SIGSEGV,但真正的问题是你能做任何有用的事情吗?你的堆栈已经是瑞士奶酪了,运行任何操作都会导致更多的堆栈使用,谁知道会发生什么。【参考方案2】:
我认为你在理解 alloca 的真正含义时需要小心一点。与进入堆的 malloc 不同,它通过存储桶和各种缓冲区的链表进行搜索,alloca 只需获取您的堆栈寄存器(x86 上的 ESP)并移动它以在线程堆栈上创建一个“洞”,您可以在其中存储任何您想要的东西。这就是为什么它超级快,只需一条(或几条)汇编指令。
因此,正如其他人所指出的,您需要担心的不是“虚拟内存”,而是为堆栈保留的大小。尽管其他人将自己限制为“几百字节”,但只要您了解您的应用程序并注意它,我们已经分配了 256kb 没有任何问题(默认堆栈大小,至少对于 Visual Studio,是 1mb,您总是可以如果需要,可以增加)。
此外,您真的不能将 alloca 用作通用分配器(即将它包装在另一个函数中),因为无论分配给您的内存是什么,当当前函数的堆栈帧被弹出时(即当函数退出)。
我也看到有人说alloca不是完全跨平台兼容的,但是如果你正在为特定平台编写特定的应用程序并且你可以选择使用alloca,有时它是你最好的选择,只要您了解增加堆栈使用量的含义。
【讨论】:
因此 alloca( ) 堆栈上的内存块比堆上快得多。但是与 malloc() 相比,访问 alloca() 分配的内存怎么样?由于内存局部性,它是否也通常更快地写入/读取?谢谢! 可能,但要记住的一件事是,现代 CPU 在涉及 L1-3 缓存以及它们所做的所有预取和疯狂的执行分叉时是如此复杂,以至于试图建立一个心理解释数据局部性和速度增益的模型几乎是不可能的。 9999/10000 次您不会注意到或关心性能提升。在极少数情况下,您正在优化一段非常关键的代码中的非常紧凑的循环,您最好进行试验,看看哪些更改具有性能增益。否则内存就是内存,访问速度可能相同【参考方案3】:首先,这是因为alloca
内存很难控制。它是无类型的,尽早死亡,这使得它不是很有帮助。此外,alloca
有一些不幸的副作用,这些副作用是现在必须动态索引常规堆栈变量而不是常量,这甚至会影响您在访问它们的基本操作时的性能,并消耗寄存器/堆栈空间来存储动态偏移量。这意味着使用alloca
的实际成本并不仅仅记录在函数返回所需的时间中。此外,与堆内存相比,堆栈内存非常有限——我相信在 Windows 上,堆栈限制默认为 8MB,而堆几乎可以是整个用户地址空间。更重要的是,最终,您想要返回的任何数据都必须在堆上,因此您不妨将其用作工作空间。
【讨论】:
您确定事情是这样工作的吗?您的建议暗示编译器要么具有alloca 的静态知识,要么它们是运行时机器,可以执行您所说的操作。例如,在遇到 alloca 使用时创建偏移表的运行时机制? @Hassan Syed:alloca
不是一个真正的函数。编译器必须对其进行特殊处理。它是静态完成的。
不是函数栈开始处的变量(以及之后分配的内存)吗?至少对于在 alloca 调用之前声明的变量?【参考方案4】:
有一点没有发afai可以看到是stack is often contiguous,而堆没有。说堆栈内存不足的可能性与堆内存不足的可能性一样,通常是不正确的。
在 C++ 中,将对象实例声明为局部变量是很常见的,这有点像 alloca
,但它是结构化内存而不是 N 字节块 - 也许您可以将其视为对您的 main 的致敬点,那就是更多地使用基于堆栈的内存是一个好主意。与在 C++ 程序中使用 malloc
(或 alloca
)相比,我更愿意这样做(将对象实例声明为 RAII 本地)。所有那些 free
调用以使异常安全......
这通常假定对象的范围仅限于此函数及其调用的函数。如果不是这样,那么使用基于堆栈的内存通常不是一个好主意。
【讨论】:
【参考方案5】:windows 堆栈不会增长 - 它的保留大小是在链接时设置的,但只有在需要时才会提交此大小内的页面。见http://msdn.microsoft.com/en-us/library/ms686774%28v=vs.85%29.asp。由于默认保留大小为 1Mb,因此在使用 alloca()
时很容易超过此大小。
【讨论】:
你说得对,我忘记了细节,不过,我们正在谈论一个重要的限制。我可以设想一些超过 1MB 的应用程序。但我希望总体保留的虚拟地址限制至少在 32-128 mb 之间(在 32 位系统上)。我想我必须调查一下。以上是关于关于alloca的使用和滥用的主要内容,如果未能解决你的问题,请参考以下文章