固定大小的数组与 alloca(或 VLA)

Posted

技术标签:

【中文标题】固定大小的数组与 alloca(或 VLA)【英文标题】:Fixed size array vs alloca (or VLAs) 【发布时间】:2017-02-11 21:15:19 【问题描述】:

什么时候alloca() 比通过声明一个固定大小的数组在堆栈上分配的内存更可取?


详情:

众所周知,alloca() 是一个有争议的函数。乱用,会导致栈溢出。明智地使用,它可以通过避免堆分配从紧密循环中减少几纳秒。在 this question 关于为什么 alloca 被认为不好时,一些最重要的答案主张偶尔使用 alloca

另一种从堆栈分配的方法是简单地声明一个固定大小的数组。这种策略的一个例子可以在Howard Hinnant's stack allocator 的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。如果这种理解是错误的,请提及它。

【问题讨论】:

这是C 代码。 1) malloc 调用未强制转换——这不适用于 C++,并且 2) C++ 语言中没有 VLA。 如果函数被递归调用,一个小的过度分配很快就会变成一个巨大的过度分配。 我在这里主要是出于假设,所以不要引用我的话。我想不出任何理由,它会分配比请求的确切数量更多的东西。 malloc 必须考虑以一种可以有效释放重新分配内存的方式管理内存。在堆栈上,它可以将堆栈指针移回任何需要的位置,并完成它。 @Riley 我怀疑alloca 通常不需要进入内核模式。如果是这样,它可能只需要扩展堆栈空间,这不会在每次调用时发生。我不知道如何实际确定 glibc 函数是否进入内核模式。 strace编译并运行一个简单的测试后,alloca似乎没有进行系统调用。因此,它不应该比固定数组慢很多。 alloca 内存不足时不会发出任何警告,它只是 UB see here 【参考方案1】:

使用alloca() 与固定大小的数组有哪些权衡?

    便携性。 alloca() 不是标准 C 库函数。固定大小的数组是语言的一部分。

    可分析性。分析代码内存使用情况的工具定期支持通过固定侧阵列进行堆栈深度分析。 alloc() 可分析性可能存在也可能不存在。

    空间效率。 alloca() 分配被禁止的内存空间。固定大小的数组往往会过度分配。

    代码效率/速度肯定是一个实现问题,需要进行分析来比较性能。预计不会有显着差异。

    VLA 的优点/缺点类似于 alloca(),只是它是 C99 标准的一部分,但在 C11 中只是可选的。

【讨论】:

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

以上是关于固定大小的数组与 alloca(或 VLA)的主要内容,如果未能解决你的问题,请参考以下文章

GCC 如何实现变长数组?

如果存在 VLA,为啥仍然需要 malloc? [复制]

我们可以在 c++20 协程中使用 alloca() 或可变长度数组扩展吗?

使用可变长度数组是不是安全?

为啥零长度 VLA 是 UB?

在 MS Visual C++ 中启用 VLA(可变长度数组)?