对alloca的使用和滥用

24
我正在开发一个软实时事件处理系统。我希望尽可能减少代码中具有非确定性时间的调用。我需要构建一个包含字符串、数字、时间戳和GUID的消息,可能使用std::vectorboost::variant

在过去的类似项目中,我一直想使用alloca函数。然而,在研究系统编程文献时,总是会对这个函数调用提出巨大的警告。就我个人而言,过去15年中没有一台服务器级别的机器没有虚拟内存,我还知道Windows堆栈一页一页地增长虚拟内存,因此我认为UNIX也是如此。这里没有任何难以逾越的障碍了(不再存在),堆栈空间和堆一样容易用完,那么问题来了?为什么人们不疯狂地追捧alloca呢?我可以想到许多使用alloca的负责任的用例(比如字符串处理)。

无论如何,我决定测试性能差异(见下文),并且alloca和malloc之间有5倍的速度差异(测试涵盖了我如何使用alloca)。所以,情况是否已经改变?我们是否应该放弃谨慎,只要我们能绝对确定对象的生命周期,就使用alloca(包装在std::allocator中)呢?

我已经厌倦了生活中的恐惧!

编辑:

好吧,这里有限制:对于Windows系统,它是一个链接时的限制。对于Unix系统,似乎是可以调整的。需要一个页面对齐内存分配器: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的测试结果不正确。由于在LLVM IR代码级别上没有副作用或使用其结果,因此对alloca的内部调用被优化掉了(删除了)。 - osgx
我曾与那些制造通用嵌入式系统硬件的工程师(EE)一起工作,例如电缆公司的网关。他们分配了一个固定大小的缓冲区,然后重复使用它。他们从未进入内存管理器。 - jww
5个回答

18

首先,虚拟内存的数量很多,并不意味着您的进程将被允许填满它。在*nix系统中,有堆栈大小限制,而堆则更加宽容。

如果您只需要分配几百/几千字节,那么可以继续进行。任何超出此范围的内容都将取决于系统上设置的限制(ulimit),这只会导致灾难。

为什么使用alloca()不被认为是良好的实践?

在我的工作开发框(Gentoo)上,我有一个默认的堆栈大小限制为8192 kb。这并不算很大,如果alloca溢出堆栈,则其行为是未定义的。


我想知道,你是否知道在Unix上是否有某种受控的退出信号来处理堆栈溢出问题? - Hassan Syed
1
快速执行 man 7 signal 并没有显示任何内容。据我所知,一个严重的堆栈溢出会导致 SIGSEGV。如果我没记错的话,你可以捕获 SIGSEGV,但真正的问题是你能做些什么有用的吗?你的堆栈已经像瑞士奶酪一样了,运行任何操作都会导致更多的堆栈使用,而且谁知道会发生什么。 - Chris Eberle

6
我认为你需要小心理解 alloca 的实际含义。与 malloc 不同,后者在堆中搜索各种缓冲区的桶和链表,而 alloca 仅仅使用栈寄存器(x86 上的 ESP),并将其移动以在线程栈上创建一个“空洞”,您可以在其中存储任何内容。这就是为什么它非常快速,只需要一条或几条汇编指令。

因此,正如其他人所指出的那样,你不必担心“虚拟内存”,而是要关注保留给堆栈的大小。虽然其他人限制自己只分配“几百字节”,但是只要您了解您的应用程序并仔细考虑,我们已经分配了高达256kb的内存而没有遇到任何问题(默认堆栈大小,至少对于 Visual Studio 而言,是1mb,如果需要,您总是可以增加它)。

另外,你真的不能将 alloca 用作通用分配器(即在另一个函数中包装它),因为 alloca 分配给您的任何内存都会在当前函数的堆栈帧弹出时消失(即当函数退出时)。

我也看到一些人说 alloca 不完全跨平台兼容,但是如果您正在为特定平台编写特定应用程序,并且可以使用 alloca 选项,有时它是您最好的选择,只要您了解增加堆栈使用的影响。

所以,在堆栈上使用alloca()动态分配内存块比在堆上更快。但是,与malloc()分配的内存相比,访问由alloca()分配的内存速度也更快吗?这是不是因为内存局部性通常更快?谢谢! - dragonxlwang
1
可能,但需要记住的一件事是现代CPU在L1-3缓存、预取和疯狂的执行分叉方面非常复杂,试图建立一个可以解释数据局部性和速度提升的心理模型几乎是不可能的。9999/10000次你都不会注意或关心性能提升。只有极少数情况下,在优化非常关键的代码中的一个非常紧密的循环时,你最好是进行实验,看哪种改变可以提高性能。否则内存就是内存,访问速度可能是相同的。 - DXM

4
首先,alloca分配的内存非常难以控制。它没有类型,最早的机会就会消失,这使得它不是很有帮助。此外,alloca有一些不幸的副作用,那就是常规栈变量现在必须动态索引而不是常量,这可能会影响您访问它们的基本操作性能并消耗寄存器/堆栈空间以存储动态偏移量。这意味着使用alloca的真正成本不仅仅记录在函数返回所需的时间上。此外,与堆内存相比,栈内存非常有限-在Windows上,默认情况下堆栈限制为8MB,而堆几乎可以占用整个用户地址空间。更重要的是,无论您想要返回什么数据,都必须在堆上,因此您可能只是使用堆作为工作空间。

你确定这就是事情的运作方式吗?你所建议的意味着编译器要么具有对alloca的静态知识,要么存在运行时机制来执行你所说的操作。例如,当遇到alloca使用时创建偏移表的运行时机制? - Hassan Syed
2
@Hassan Syed:alloca不是一个真正的函数。编译器必须特殊处理它。这是静态完成的。 - Puppy
变量不是在函数堆栈的开头(然后才是alloca分配的内存)吗?至少对于alloca调用之前声明的变量是这样吧? - aberaud

3
据我所知,一个尚未提及的观点是堆栈通常是连续的,而堆不是。一般来说,不能说堆和栈一样容易耗尽内存。
在C++中,很常见地看到对象实例被声明为局部变量,这有点像结构化内存的alloca,而非N字节的块 - 也许你可以将其视为对您的主要观点的致敬,即更多地使用基于堆栈的内存是一个好主意。我宁愿这样做(将对象实例声明为RAII局部变量),而不是在C++程序中使用malloc(或alloca)。所有这些free调用都是为了使异常安全...
这通常假定对象的范围限制在此函数及其调用函数中。如果不是这种情况,那么通常不建议使用基于堆栈的内存。

2

你说得对,我忘记了细节,但我们仍在谈论一个重要的限制。我可以想象一些应用程序超过1MB。但我预计总保留的虚拟地址限制至少为32-128 MB(在32位系统上)。我猜我需要进一步研究一下。 - Hassan Syed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接