使用可变长度数组是不是有任何开销?

Posted

技术标签:

【中文标题】使用可变长度数组是不是有任何开销?【英文标题】:Is there any overhead for using variable-length arrays?使用可变长度数组是否有任何开销? 【发布时间】:2011-01-03 07:57:19 【问题描述】:

使用可变长度数组是否有一些开销?数组的大小可以在运行时通过命令行参数传递吗?相比自动动态分配数组,为什么要引入它?

【问题讨论】:

【参考方案1】:

VLA 确实有一些开销(与“普通”命名的编译时大小的数组相比)。

首先,它具有运行时长度,但该语言为您提供了在运行时获取数组实际大小的方法(使用sizeof)。这立即意味着必须将数组的实际大小存储在某个地方。这会导致一些微不足道的每阵列内存开销。然而,由于 VLA 只能被声明为自动对象,因此这种内存开销是任何人都不会注意到的。就像声明一个额外的整型局部变量一样。

其次,VLA 通常分配在堆栈上,但由于它的可变大小,通常情况下它在内存中的确切位置在编译时是未知的。出于这个原因,底层实现通常必须将其实现为指向内存块的指针。这引入了一些额外的内存开销(对于指针),由于上述原因,这又是完全无关紧要的。这也带来了轻微的性能开销,因为我们必须读取指针值才能找到实际的数组。这与访问 malloc-ed 数组时的开销相同(而不是使用命名的编译时大小的数组)。

由于 VLA 的大小是一个运行时整数值,它当然可以作为命令行参数传递。 VLA 不在乎它的大小来自哪里。

VLA 是作为运行时大小的数组引入的,具有低分配/解除分配成本。它们适合“普通”命名的编译时大小的数组(其分配释放成本几乎为零,但大小固定)和malloc-ed 数组(具有运行时大小,但分配释放成本相对较高) .

VLA [几乎] 遵守与自动(即本地)对象相同的依赖于范围的生命周期规则,这意味着在一般情况下它们不能替换 malloc-ed 数组。它们的适用性仅限于需要具有典型自动生命周期的快速运行时大小的数组的情况。

【讨论】:

VLA 实际上遵循与其他自动对象几乎相同的生命周期规则(“从 [VLA] 的声明到程序执行离开声明的范围”与“从进入块[对象]与之关联,直到该块的执行以任何方式结束”)[来自 C99 标准的 6.2.4(5) 和 6.2.4(6)]。 "VLA 通常分配在堆栈上," -- 通常?你的意思是它可能是在堆上分配的? @Cool Guy:我的意思是语言规范没有指定它们的分配位置,甚至没有假设“堆栈”的存在,因此我通常更喜欢在每个是时候我谈论一些正式的实现细节了。 分配后,malloc( ) 分配变量和 alloca( ) 分配变量有什么区别吗?例如,加载/写入变量 @dragonxlwang:一旦分配,就没有区别了。 (除了诸如内存局部性之类的考虑之外:alloca 在其他局部变量旁边“就在堆栈中”分配内存,而malloc 在“堆中某处很远的地方”分配内存。)【参考方案2】:

可变长度数组存在一些运行时开销,但您必须相当努力地测量它。请注意,如果 vla 是可变长度数组,则 sizeof(vla) 不是编译时常量。

数组的大小可以在运行时传递给函数。如果您选择从命令行参数中获取大小并将其转换为整数并在运行时将其传递给函数,那就这样吧 - 它会起作用。

使用可变长度数组是因为变量会自动分配到正确的大小,并在退出函数时自动释放。这避免了过度分配空间(当您主要使用最小尺寸时,为最大可能尺寸分配足够的空间),并避免内存清理问题。

此外,对于多维数组,AFAIK 它的行为更像 Fortran - 您可以动态配置所有维度,而不是被固定在数组的前导维度之外的所有维度上。


VLA 的一些运行时开销的具体证据 - 至少在 SPARC (Solaris 10) 上使用 GCC 4.4.2。

考虑以下两个文件:

vla.c - 使用变长数组

#include <assert.h>
#include <stddef.h>
extern size_t identity_matrix(int n, int m);

size_t identity_matrix(int n, int m)

    int vla[n][m];
    int i, j;
    assert(n > 0 && n <= 32);
    assert(m > 0 && m <= 32);
    for (i = 0; i < n; i++)
    
        for (j = 0; j < m; j++)
        
            vla[i][j] = 0;
        
        vla[i][i] = 1;
    
    return(sizeof(vla));

fla.c - 使用固定长度数组

#include <assert.h>
#include <stddef.h>
extern size_t identity_matrix(int n, int m);

size_t identity_matrix(int n, int m)

    int fla[32][32];
    int i, j;
    assert(n > 0 && n <= 32);
    assert(m > 0 && m <= 32);
    for (i = 0; i < n; i++)
    
        for (j = 0; j < m; j++)
        
            fla[i][j] = 0;
        
        fla[i][i] = 1;
    
    return(sizeof(fla));

编译和目标文件大小

为了比较,本地数组的名称不同(vla vs fla),声明时数组的维度也不同,否则文件相同。

我编译使用:

$ gcc -O2 -c -std=c99 fla.c vla.c

目标文件的大小有些不同 - 用“ls”和“size”衡量:

$ ls -l fla.o vla.o
-rw-r--r--   1 jleffler rd          1036 Jan  9 12:13 fla.o
-rw-r--r--   1 jleffler rd          1176 Jan  9 12:13 vla.o
$ size fla.o vla.o
fla.o: 530 + 0 + 0 = 530
vla.o: 670 + 0 + 0 = 670

我没有进行广泛的测试来查看有多少开销是固定的,有多少是可变的,但是使用 VLA 会有开销。

【讨论】:

行“vla[i][i] = 1;”需要一个额外的断言(n == m)。最好将“vla[i][j] = ? i==j ? 1: 0;”放在内部循环中。 YMMV。【参考方案3】:

我只是想知道使用可变长度数组是否有一些开销?

没有

可以在运行时通过命令行参数传递数组的大小吗?

是的。

与自动和动态分配数组相比,为什么要引入它?

自动分配只允许在编译时已知的固定大小。

动态分配(malloc)会将数组存储在上,内存空间大,但访问速度较慢。

VLA 通过将数组放入堆栈 来工作。这使得分配和访问非常快,堆栈通常很小(几 KB),当 VLA 溢出堆栈时,它与无限递归无法区分。

【讨论】:

哇——我们的答案的时机真是太棒了! 并且,请参阅我的(修改后的)答案以说明使用 VLA 存在一些运行时开销,至少在某些编译器实现中(在 Sun SPARC 和 Solaris 10 上使用 GCC 4.4.2 作为具体例子)。 没有理由认为堆访问速度较慢。分配和释放比堆栈分配和释放慢(只需要调整堆栈指针),但是一旦分配了一个对象,它就只是内存中的另一个对象。 @KeithThompson:嗯,内存缓存? (如何)计算出 VLA 的最大允许大小,如果超过了会发生什么? (欢迎参考标准。)【参考方案4】:

VLA 的开销应该很小(最多应该导致堆栈指针的添加)。动态分配需要手动内存管理并且比基于堆栈的 VLA 分配要慢,并且数组的“自动”声明需要数组大小的编译时表达式。但是请记住,如果发生堆栈溢出,则会导致未定义的行为,因此请保持 VLA 相对较小。

您可以通过命令行参数传递数组的大小,但您必须自己编写代码来处理。

【讨论】:

以上是关于使用可变长度数组是不是有任何开销?的主要内容,如果未能解决你的问题,请参考以下文章

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

将 alloca() 用于可变长度数组是不是比在堆上使用向量更好?

通过过度分配内存在结构中内联可变长度数组是不是有效?

C# 长度为 1 的数组与单值、性能和内存开销

创建具有未指定可变行长度的多维数组(Java)

用于可变长度参数数组的 PHPDoc