编译器如何在编译时不知道大小的情况下分配内存?
Posted
技术标签:
【中文标题】编译器如何在编译时不知道大小的情况下分配内存?【英文标题】:How does the compiler allocate memory without knowing the size at compile time? 【发布时间】:2018-03-05 08:31:14 【问题描述】:我编写了一个 C 程序,它接受来自用户的整数输入,用作整数数组的大小,并使用该值声明一个给定大小的数组,我通过检查大小来确认它数组。
代码:
#include <stdio.h>
int main(int argc, char const *argv[])
int n;
scanf("%d",&n);
int k[n];
printf("%ld",sizeof(k));
return 0;
令人惊讶的是它是正确的!该程序能够创建所需大小的数组。
但是所有的静态内存分配都是在编译时完成的,而在编译时n
的值是未知的,那么编译器为什么能够分配所需大小的内存呢?
如果我们可以像这样分配所需的内存,那么使用malloc()
和calloc()
进行动态分配有什么用?
【问题讨论】:
为什么要这样做,而不是正常的“k = (int *) calloc (n, sizeof (int));”?只是为了混淆你的代码? @jamesqfint k[n];
是k = (int *) calloc (n, sizeof (int));
的混淆版本吗?我认为前者更具可读性(如果您知道存在 VLA)。
@jamesqf:性能。将 n
加载到 rsi
(准备成为 x86-64 SysV ABI 中 printf 的第二个参数)后,sub rsp, rsi
(一个简单的 asm 指令)比函数便宜得多 -致电calloc
。虽然在这种情况下,k[]
本身并没有被使用,只有sizeof(k)
,所以一个好的编译器不会在调用printf
之前实际保留堆栈空间。堆栈内存在 L1D 缓存和 TLB 中已经很热,因此它是小缓冲区的好地方。发布它也非常便宜,而且你不可能每个人都会出错,因为编译器会为你做。
@jamesqf:它不会检查大小,也不会优雅地失败。程序员不能编写使用对于他们关心的实现来说太大的 VLA 的程序。 (例如8MB stack size in new user-space threads on Linux x86-64)。通常,如果您触摸堆栈底部下方的内存并且操作系统认为这太多并且不会增加您的堆栈映射,那么您会出现段错误。在非叶函数中使用大型 VLA 与可能也使用 VLA 的子函数一起使用是个坏主意。
@jamesqf:这听起来比new
/ delete
差很多,但是对于过度使用内存的现代操作系统,情况几乎没有更糟。您可以分配比操作系统拥有的物理 RAM + 交换空间更多的 RAM,并且触摸它可能会导致内核决定终止您的进程。 (Linux 称之为 OOM 杀手)。 linuxdevcenter.com/pub/a/linux/2006/11/30/…。但是,您可以通过设置进程可以分配的虚拟内存量限制来优雅地失败分配,因此malloc
实际上会返回 NULL,但这不是默认值。
【参考方案1】:
这个结构的内存,称为“可变长度数组”,VLA,以类似于alloca
的方式在堆栈上分配。这究竟是如何发生的取决于您使用的编译器,但本质上是在已知大小时计算大小,然后从堆栈指针中减去 [1] 总大小。
你确实需要malloc
和朋友,因为当你离开这个函数时,这个分配“死”了。 [而且它在标准 C++ 中无效]
[1] 对于使用“向零增长”的堆栈的典型处理器。
【讨论】:
在 C++ 中,我相信你知道,你可以使用std::vector
来表示一个数组,它的大小可以在运行时确定,并且它的生命周期是当前作用域。您还可以将std::vector
用于许多其他目的。所以有一个相近的替代品。
@Davislor 虽然这不会在堆栈中。
@lalala std::vector
允许您指定它将使用的std::allocator
。 IIRC,尽管我不认为该标准可以保证某些东西使用堆栈。但如果你真的想要一个调用alloca()
的std::vector
,你可以得到一个。 (不过,您通常不会,因为它们可以调整大小。)我很确定一个实现甚至可以用 C++ 编写它的 C 运行时,并在后台使用 C++ std::vector
实现 C VLA!
@Davislor: std::get_temporary_buffer
与 gcc/clang 中的 VLA 或 alloca
完全不同。他们依次尝试更小的new
:godbolt.org/g/VTfNzi,遵循标准的字母并尝试分配一些东西,即使它比请求的小。您必须使用std::return_temporary_buffer
释放。 (OTOH,如果编译器可以证明指针不会转义,那么它可以只保留堆栈空间并使return_temporary_buffer
成为空操作。但真正的编译器不会。)
longjmp
明确不需要释放 VLA 存储空间。这在 C11 7.13.2.1 中被提及。我认为这是专门允许编译器在堆栈分配困难的情况下以与实现std::vector
或malloc
/free
对的方式相同的方式实现VLA。【参考方案2】:
这不是“静态内存分配”。您的数组k
是一个可变长度数组 (VLA),这意味着该数组的内存是在运行时分配的。大小将由n
的运行时值决定。
语言规范没有规定任何特定的分配机制,但在典型的实现中,您的k
通常最终会成为一个简单的int *
指针,实际内存块在运行时在堆栈上分配。
对于 VLA sizeof
运算符也在运行时进行评估,这就是您在实验中从中获得正确值的原因。只需使用%zu
(不是%ld
)来打印size_t
类型的值。
malloc
(和其他动态内存分配函数)的主要目的是覆盖适用于本地对象的基于范围的生命周期规则。 IE。用malloc
分配的内存“永远”分配,或者直到你用free
明确地取消分配它。使用malloc
分配的内存不会在块末尾自动释放。
在您的示例中,VLA 不提供这种“破坏范围”功能。您的数组k
仍然遵守常规的基于范围的生命周期规则:它的生命周期在块的末尾结束。因此,一般情况下,VLA不可能替代malloc
等动态内存分配函数。
但在特定情况下,当您不需要“击败作用域”而只需使用malloc
来分配运行时大小的数组时,VLA 确实可能被视为malloc
的替代品。请再次记住,VLA 通常是在堆栈上分配的,直到今天在堆栈上分配大块内存仍然是一种相当有问题的编程实践。
【讨论】:
@Rahul:C 不支持static
VLA。如果 n
是运行时值,则不允许使用 static int k[n]
。但即使它被允许,它也不会每次都分配一个新的内存块。同时。 malloc
每次调用时都会分配一个新块。所以,这里与malloc
没有相似之处,即使是static
。
“在典型的实现中,你的 k 最终将成为一个简单的 int * 指针”中的措辞似乎有点冒险。关于指针和数组,存在很多混淆。
@Rahul:VLA 是在 C99 标准中引入的。从形式上讲,它们已经存在了大约 18 年。当然,编译器支持需要一些时间。
好吧,我绝对不会称它为 *int *
指针”,因为这里没有这样的 C 对象。
@rcgldr:VLA 与alloca
非常相似,并且绝对受到alloca
的“启发”。但是,alloca
分配具有“函数”生命周期的内存:内存将持续存在,直到函数退出。 IE。它忽略了除了最外面的所有块边界 - 函数体本身。同时 VLA 具有正常的基于块的生命周期。在这方面,VLA 与alloca
非常不同。确实,Visual Studio 直到今天都不支持 VLA。然而,由于 C11 VLA 是该语言的一个可选功能,因此毫无价值。【参考方案3】:
在 C 中,编译器支持 VLA(可变长度数组)的方式取决于编译器 - 它不必使用 malloc()
,并且可以(并且经常)使用有时称为“堆栈”的东西" 记忆 - 例如使用不属于标准 C 的alloca()
等系统特定函数。如果确实使用堆栈,则数组的最大大小通常比使用malloc()
可能小得多,因为现代操作系统允许程序的配额要小得多堆栈内存。
【讨论】:
现代操作系统(在某些情况下甚至是旧的和嵌入式操作系统)允许用户配置堆栈大小【参考方案4】:可变长度数组的内存显然不能静态分配。但是,它可以在堆栈上分配。通常,这涉及使用“帧指针”来跟踪函数堆栈帧的位置,以应对堆栈指针的动态确定变化。
当我尝试编译您的程序时,似乎实际发生的情况是可变长度数组得到了优化。所以我修改了你的代码以强制编译器实际分配数组。
#include <stdio.h>
int main(int argc, char const *argv[])
int n;
scanf("%d",&n);
int k[n];
printf("%s %ld",k,sizeof(k));
return 0;
使用 gcc 6.3 为 arm 编译的 Godbolt(使用 arm,因为我可以读取 arm ASM)将其编译为 https://godbolt.org/g/5ZnHfa。 (我的cmets)
main:
push fp, lr ; Save fp and lr on the stack
add fp, sp, #4 ; Create a "frame pointer" so we know where
; our stack frame is even after applying a
; dynamic offset to the stack pointer.
sub sp, sp, #8 ; allocate 8 bytes on the stack (8 rather
; than 4 due to ABI alignment
; requirements)
sub r1, fp, #8 ; load r1 with a pointer to n
ldr r0, .L3 ; load pointer to format string for scanf
; into r0
bl scanf ; call scanf (arguments in r0 and r1)
ldr r2, [fp, #-8] ; load r2 with value of n
ldr r0, .L3+4 ; load pointer to format string for printf
; into r0
lsl r2, r2, #2 ; multiply n by 4
add r3, r2, #10 ; add 10 to n*4 (not sure why it used 10,
; 7 would seem sufficient)
bic r3, r3, #7 ; and clear the low bits so it is a
; multiple of 8 (stack alignment again)
sub sp, sp, r3 ; actually allocate the dynamic array on
; the stack
mov r1, sp ; store a pointer to the dynamic size array
; in r1
bl printf ; call printf (arguments in r0, r1 and r2)
mov r0, #0 ; set r0 to 0
sub sp, fp, #4 ; use the frame pointer to restore the
; stack pointer
pop fp, lr ; restore fp and lr
bx lr ; return to the caller (return value in r0)
.L3:
.word .LC0
.word .LC1
.LC0:
.ascii "%d\000"
.LC1:
.ascii "%s %ld\000"
【讨论】:
【参考方案5】:当说编译器在编译时为变量分配内存时,这意味着这些变量的位置由编译器生成的可执行代码决定并嵌入,而不是编译器在工作时为它们腾出空间。 实际的动态内存分配由生成的程序在运行时进行。
【讨论】:
以上是关于编译器如何在编译时不知道大小的情况下分配内存?的主要内容,如果未能解决你的问题,请参考以下文章