固定大小数组 vs alloca(或可变长度数组)

5

什么情况下使用alloca()比声明固定大小数组分配在堆栈上的内存更好?


详情:

我们知道,alloca() 是一个有争议的函数。如果使用不当,它可能会导致堆栈溢出。但如果使用得当,它可以通过避免堆分配,在紧密循环中节省几个纳秒。在关于为什么 alloca 被认为是不好的这个问题中,一些顶级答案主张偶尔使用 alloca

另一种从堆栈分配内存的方法是简单地声明一个固定大小的数组。在Howard Hinnant的堆栈分配器arena 类中就有这种策略的示例。 (当然,那段代码是 C++ 的,但这个概念仍然适用于 C。)

使用 alloca 和使用固定大小的数组有哪些权衡之处?什么时候,如果有的话,一个明显优于另一个?这是否仅仅是一个性能问题,应该在每个单独的情况中进行实证测试(当性能是关键目标,已经确定了热点时)? 固定大小的数组更为悲观 - 它总是分配我们愿意在堆栈上分配的内存大小 - 但这是否是好事还是坏事尚不清楚。

为了尽可能清晰,这里有一个非常简单的示例,其中存在使用 alloca 或固定大小数组的理由:

#include <alloca.h>
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

void foo_alloca(const size_t mem_needed) {
    printf("foo_alloca(%zu)\n", mem_needed);
    char* mem;
    bool used_malloc = false;
    if (mem_needed <= 100)
        mem = alloca(mem_needed);
    else {
        mem = malloc(mem_needed);
        used_malloc = true;
    }
    assert(mem_needed != 0);
    // imagine we do something interesting with mem here
    mem[0] = 'a';
    mem[1] = 'b';
    mem[2] = 'c';
    mem[3] = '\0';
    puts(mem);
    if (used_malloc)
        free(mem);
}

void foo_fixed(const size_t mem_needed) {
    printf("foo_fixed(%zu)\n", mem_needed);
    char* mem;
    char stack_mem[100];
    bool used_malloc = false;
    if (mem_needed <= 100)
        mem = stack_mem;
    else {
        mem = malloc(mem_needed);
        used_malloc = true;
    }
    assert(mem_needed != 0);
    // imagine we do something interesting with mem here
    mem[0] = 'a';
    mem[1] = 'b';
    mem[2] = 'c';
    mem[3] = '\0';
    puts(mem);
    if (used_malloc)
        free(mem);
}

int main()
{
    foo_alloca(30);
    foo_fixed(30);
    foo_alloca(120);
    foo_fixed(120);
}

另一个与alloca非常相似的选项是VLA。据我了解,从alloca和VLA获得的内存基本上具有相同的行为,因此该问题也适用于VLA。如果这种理解是错误的,请提出来。


1
如果函数被递归调用,小的过度分配很快就会变成巨大的过度分配。 - Riley
1
我这里大多数是基于假设,所以不要引用我的话。我想不出任何理由它会分配比请求的精确数量更多的内存。malloc必须考虑以一种管理内存的方式,使其能够高效地释放和重新分配内存。在堆栈上,它只需将堆栈指针向后移动到需要的位置,然后就完成了。 - Riley
1
@Riley 我怀疑 alloca 通常不需要进入内核模式。如果需要,它可能只需要扩展堆栈空间,这不会在每次调用时发生。但我不知道如何确定 glibc 函数是否进入内核模式。 - Praxeolitic
哎呀,我没想到。我只是假设它会成为一个系统调用,因为大多数其他的内存管理函数都是这样,所以它可能实际上并不会成为一个系统调用。 - Riley
1
编译并使用 strace 运行简单测试后,似乎 alloca 并不会进行系统调用。因此,它不应比固定数组慢太多。当内存耗尽时,alloca 不会给出任何警告,只是 UB(未定义行为),请参见这里 - Riley
显示剩余10条评论
1个回答

4
使用 alloca() 与使用固定大小的数组相比有以下权衡考虑:
  1. 可移植性。 alloca() 不是标准的 C 库函数,而固定大小的数组则是语言的一部分。

  2. 可分析性。工具通常通过对固定大小数组的堆栈深度分析来分析代码的内存使用情况。可能存在 alloc() 的分析能力,也可能不存在。

  3. 空间效率。 alloca() 分配规定的内存空间,而固定大小的数组往往会过度分配。

  4. 代码效率/速度肯定是一个实现问题,需要进行性能分析以进行比较。不应该期望有显著的差异。

  5. 可变长度数组的利弊与 alloca() 类似,只不过它是 C99 标准的一部分,但在 C11 中仅为可选项。


你能举个分析堆栈深度的工具的例子吗?我不熟悉这种分析。谢谢。 - Praxeolitic
@Praxeolitic 首先想到的是:CCS 分析堆栈/内存使用情况。通过不允许递归,可以计算出绝对最大的堆栈深度/内存使用情况,这在工作于受限嵌入式内存环境的编译器中非常重要。 - chux - Reinstate Monica

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