C99中变长数组的内存分配策略

blue_zyb 2006-10-19 10:35:49
在C99中新加入了对变长数组的支持,也就是说数组的长度可以由变量来定义。

对于下面的代码:
int main()
{
int n;
scanf("%d", &n);
int ar[n];
printf("%d\n", sizeof(ar));

return 0;
}
是可以成立的。

这与之前的C标准有很大不同,以前的数组大小只能由常量来指定,也就是编译器在编译时就可以确定为数组分配多少存储空间,一般来说,一个活动记录的大小在进入一个函数的时候就可以确定下来。这个变长数组的特性必定把数组的分配引入了运行时期,附带的,sizeof这个原来的编译期操作也不得不被带入到运行时期。那么,对于动态数组的实现,编译器必定要生成一些代码来对其进行runtime的支持,这必定有损效率。

我们暂且不去评论这种特性的优与劣。就动态数组在栈内(试验过global的变长数组不支持)的存储分配策略到底是如何,请大家发表见解。
...全文
1732 32 打赏 收藏 转发到动态 举报
写回复
用AI写文章
32 条回复
切换为时间正序
请发表友善的回复…
发表回复
chenhu_doc 2006-10-20
  • 打赏
  • 举报
回复
现留名
blue_zyb 2006-10-20
  • 打赏
  • 举报
回复
Oh my god!
上面那一段描述中全部用的“动态数组”,汗。。。
也就是下面这一段文字中的“动态数组”全部改为“变长数组”,对不起了。。

// start

当有多个变长数组分配时,也就是编译器不能用仅有的几个寄存器保存当前的esp时,编译器就会划分一块区域(这块区域也在栈中,而且是先于变长数组分配划分好的)来记录每个数组的首地址。例如,我昨晚试验的程序有9个变长数组,前三个数组的首地址存在三个通用寄存器中,而后面的6个的首地址则放在比如说ebp-40,ebp-44,ebp-48...的位置。然后如果引用第四个数组的元素,比如源代码是ar4[1] = 1;编译器会先取ebp-40的内容到一个临时寄存器,再用该值索引数组。也就是有类似如下的汇编代码:
movl (%ebp-40), %eax
movl $1, 4(%eax)

也就是说,从一个单一的概念模型上来说,对于碰到变长数组的情形,编译器可以按一个指针的大小为其预留一个slot,然后到运行的时候esp-eax分配了空间以后,把当前esp,即数组的首地址放入到这个slot中。以后对数组的引用,就要进行两次访存,一次取到数组的首地址,一次访问真正的数组元素。这与以前的数组访问的开销是不同的,以前的数组元素访问之需要一次访存操作,而变长数组的下标访问有点类似于指针的下标访问了。

变长数组的存储分配是在运行时,并且访问也需要两次访存,比原来的数组访问开销要大,但它与动态分配malloc还是有区别的。由于变长数组分配在栈中,只需要改变esp的值就能达到分配的目的,而malloc分配则需要runtime system来进行heap management,也就是说分配的时候需要一定的search operation来得到一块连续的存储,而释放的时候也要执行相应的代码来使得这块存储available for future use。所以,变长数组的开销还是小于malloc的。

// end
lj860603 2006-10-20
  • 打赏
  • 举报
回复
关于“碎片”的问题偶也还是有点疑问,“碎片”除了用来对齐之外,多余的部分到底用来干什

么?多余部分的“碎片”到底存储了什么东西,不可能还是空的吧?

不过说到底变长数组和动态分配之间应该还是有一定的相似点,嘿嘿~~
linjixin123 2006-10-20
  • 打赏
  • 举报
回复
非动态数组应该分配在常量区,而动态数组应该是在堆栈
blue_zyb 2006-10-20
  • 打赏
  • 举报
回复
呵呵,blue_zyb() 大哥,期待你的总结,学习:-P
--------------------------------------
。。。不要叫我‘大哥’。。。
blue_zyb 2006-10-20
  • 打赏
  • 举报
回复
对于每个数组,GCC采取的分配策略确实类似于一楼pcboyxhy(-273.15℃)挑出的那几句代码:

movl -8(%ebp), %edx // 取用作数组维度的变量内容(此例中在ebp-8的位置到edx
leal 30(,%edx,4), %eax // edx * 4 + 30 到eax
andl $-16, %eax // 字节对齐(也就是eax-eax%16,按16字节对齐)
subl %eax, %esp // esp-eax,改变esp的值,分配存储空间

现在的esp对应的就是当前被分配数组的首地址。

当有多个动态数组分配时,也就是编译器不能用仅有的几个寄存器保存当前的esp时,编译器就会划分一块区域(这块区域也在栈中,而且是先于动态数组分配划分好的)来记录每个数组的首地址。例如,我昨晚试验的程序有9个动态数组,前三个数组的首地址存在三个通用寄存器中,而后面的6个的首地址则放在比如说ebp-40,ebp-44,ebp-48...的位置。然后如果引用第四个数组的元素,比如源代码是ar4[1] = 1;编译器会先取ebp-40的内容到一个临时寄存器,再用该值索引数组。也就是有类似如下的汇编代码:
movl (%ebp-40), %eax
movl $1, 4(%eax)

也就是说,从一个单一的概念模型上来说,对于碰到动态数组的情形,编译器可以按一个指针的大小为其预留一个slot,然后到运行的时候esp-eax分配了空间以后,把当前esp,即数组的首地址放入到这个slot中。以后对数组的引用,就要进行两次访存,一次取到数组的首地址,一次访问真正的数组元素。这与以前的数组访问的开销是不同的,以前的数组元素访问之需要一次访存操作,而动态数组的下标访问有点类似于指针的下标访问了。

动态数组的存储分配是在运行时,并且访问也需要两次访存,比原来的数组访问开销要大,但它与malloc分配还是有区别的。由于动态数组分配在栈中,只需要改变esp的值就能达到分配的目的,而malloc分配则需要runtime system来进行heap management,也就是说分配的时候需要一定的search operation来得到一块连续的存储,而释放的时候也要执行相应的代码来使得这块存储available for future use。所以,动态的数组的开销还是小于malloc的。

以上是我的个人意见,不对的地方还望大家指出。


还有一个问题,就是我上面所提到的‘碎片’问题:
leal 30(,%edx,4), %eax
andl $-16, %eax
确实是用作对齐的,是把实际的数组大小edx*4加30后用作16字节对齐。但是,既然是按16字节对齐的话,为什么要加30呢,加15不就行了吗??
lj860603 2006-10-20
  • 打赏
  • 举报
回复
昨天回去写了一个使用多个动态数组的程序,跟踪了一下汇编,发现了一些头绪。下面总结一下。。。
================
呵呵,blue_zyb() 大哥,期待你的总结,学习:-P
blue_zyb 2006-10-20
  • 打赏
  • 举报
回复
假设是用这些‘碎片’来记录变长数组的信息,那么‘碎片’的位置又该由什么东西指出呢?反过来说,如果碎片的位置编译器能够得到,同样的数组的首地址可以得到。
-----------------------------
昨天有点犯晕了,这句话说得很不对啊,抱歉。。。

昨天回去写了一个使用多个动态数组的程序,跟踪了一下汇编,发现了一些头绪。下面总结一下。。。
blue_zyb 2006-10-19
  • 打赏
  • 举报
回复
to: pcboyxhy(-273.15℃) ( )
我也查过汇编代码,知道是分配在栈中。

但是,好像分配出来的存储是有“碎片”的。对于下面这段
leal 30(%edx), %eax
andl $-16, %eax //字节对齐
subl %eax, %esp //分配存储空间,仍然是在栈里面

例如,假设edx中数组的大小为28,
leal 30(%edx), %eax 后eax的大小为58
andl $-16, %eax 后eax的大小为48
subl %eax,%esp 后数组分配在原esp减48的位置,多分配了20个字节的空间。

而且,当动态数组很多时,每个数组的首地址是怎么访问的呢,难道就靠编译器不停的重复使用寄存器?
lichaohui 2006-10-19
  • 打赏
  • 举报
回复
实际实现跟局部变量的数组没有太大区别,也没有什么技术难度
mLee79 2006-10-19
  • 打赏
  • 举报
回复
就是给 _alloca 个正式的名分 ....

pcboyxhy 2006-10-19
  • 打赏
  • 举报
回复
GCC编译得到代码

.file "d.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.LC1:
.string "%d\n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
pushl %ebx
subl $36, %esp
andl $-16, %esp
subl $16, %esp
movl %esp, %ebx
leal -8(%ebp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call scanf
movl -8(%ebp), %edx
addl %edx, %edx
addl %edx, %edx
leal 30(%edx), %eax
andl $-16, %eax
subl %eax, %esp
movl %edx, 4(%esp)
movl $.LC1, (%esp)
call printf
movl %ebx, %esp
xorl %eax, %eax
movl -4(%ebp), %ebx
leave
ret
.size main, .-main
.ident "GCC: (GNU) 4.0.3 (Ubuntu 4.0.3-1ubuntu5)"
.section .note.GNU-stack,"",@progbits


省略无关代码
movl -8(%ebp), %edx //n的值

addl %edx, %edx
addl %edx, %edx //edx = edx*4 ,int是4字节的,所以数组占用的空间是4*n

leal 30(%edx), %eax
andl $-16, %eax //字节对齐
subl %eax, %esp //分配存储空间,仍然是在栈里面

pcboyxhy 2006-10-19
  • 打赏
  • 举报
回复
这里的空间分配是在栈里完成的
而且没有进行系统调用
所以根本没有额外的碎片所需要的费用

多分配空间是为了进行字节对其
zhanglin03130410 2006-10-19
  • 打赏
  • 举报
回复
up
liangfei456 2006-10-19
  • 打赏
  • 举报
回复
我是新手啊
什么都不会啊 !!
还请大家照顾一下啊!!
我的QQ是446747455
hyg2008 2006-10-19
  • 打赏
  • 举报
回复
m
Muf 2006-10-19
  • 打赏
  • 举报
回复
感觉变长数组容易带来安全问题。特别是在长度没有经过校验的情况下。
ctu_85 2006-10-19
  • 打赏
  • 举报
回复
up
lj860603 2006-10-19
  • 打赏
  • 举报
回复
blue_zyb(),呵呵,偶刚去图书馆了,偶平时就只知道灌水和拿分,其他啥都不会~~

假设是用这些‘碎片’来记录变长数组的信息,那么‘碎片’的位置又该由什么东西指出呢?反过来说,如果碎片的位置编译器能够得到,同样的数组的首地址可以得到。
-------------------
在顶楼,你说了“这个变长数组的特性必定把数组的分配引入了运行时期”,所以对于数组的分配应该会有相应的指令,但是对于数组的小大那可能就无法知道,所以利用“碎片”来记录一些信息。
而“碎片”又由什么东西指出呢?CPU是按程序顺序逐条指令执行的,所以在创建变长数组的同时应该有相应的指令(是在运行时分配的,所以会有相应的指令),而从提供的汇编代码上来看,“碎片”和数组在地址上应该是相连的(从汇编代码看),而每条指令都应该有相应的CS和IP值,所以“碎片”和变长数组的地址都可以由指令的CS和IP值来确定的。CPU应该就是根据这两个数值得到数组和“碎片”的地址来确定的。

呵呵,据说你大二,在学“计算机原理”?:)
从C的栈模型来说,数据访问是根据基址+偏移来确定的。基址在进入一个函数块的时候放入ebp,而偏移是编译时确定的。
---------------------
呵呵,CPU执行方式我是在汇编里稍微学到一些的。
下面你那句话我完全同意~~
哎,感觉自己就是在根据非常微薄的一点知识和思维在乱猜-_-!!

继续关注这个帖子。。。
vs_net 2006-10-19
  • 打赏
  • 举报
回复
学习一下
加载更多回复(12)

69,371

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧