Memory Layout

今天做了一个简单的测试,简单地探究了一下GCC下的Memory Layout。

程序的运行环境如下:

1
2
Linux indy2-login0 3.10.0-327.36.3.el7.x86_64 #1 SMP Mon Oct 24 16:09:20 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-11)

程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv){
int i,j,k;
double d;
float f;
void *p;

struct fred {
int a;
short s;
double d;
double d2;
short s2;
}var;

double a[30][30];
int *pd2;
double *pd;
pd2 = (int*) malloc(sizeof(int));
pd = (double*)malloc(sizeof(double));

printf("i %d %016lx\n",sizeof(i),&i);
printf("j %d %016lx\n",sizeof(j),&j);
printf("k %d %016lx\n",sizeof(k),&k);
printf("d %d %016lx\n",sizeof(d),&d);
printf("f %d %016lx\n",sizeof(f),&f);
printf("p %d %016lx\n",sizeof(p),&p);

printf("var %d %016lx\n",sizeof(var),&var);
printf("var.a %d %016lx\n",sizeof(var.a),&var.a);
printf("var.s %d %016lx\n",sizeof(var.s),&var.s);
printf("var.d %d %016lx\n",sizeof(var.d),&var.d);
printf("var.s2 %d %016lx\n",sizeof(var.s2),&var.s2);
printf("var.d2 %d %016lx\n",sizeof(var.d2),&var.d2);

printf("a %d %016lx\n",sizeof(a),a);
printf("pd %d %016lx\n",sizeof(pd),pd);
printf("pd2 %d %016lx\n",sizeof(pd2),pd2);
}

采用gcc编译并运行这段程序,得到了如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
i 4 00007fffffffdc7c
j 4 00007fffffffdc78
k 4 00007fffffffdc74
d 8 00007fffffffdc68
f 4 00007fffffffdc64
p 8 00007fffffffdc58
var 32 00007fffffffdc30
var.a 4 00007fffffffdc30
var.s 2 00007fffffffdc34
var.d 8 00007fffffffdc38
var.s2 2 00007fffffffdc48
var.d2 8 00007fffffffdc40
a 7200 00007fffffffc010
pd 8 0000000000602030
pd2 8 0000000000602010

可以看到,对于一个primitive,在该测试环境下,变量在stack上由高向低分配。对于一个结构体内部,所有的变量则在结构体对应的地址空间上由低向高顺序分配。

这里注意到,对于var这个由1个int,2个short,2个double组成的结构体,理论上,这个结构体应该占据24byte的空间,然而实际上var这个结构体占据了32byte。这是由于GCC下会对该程序进行一个内存对齐(alignment)。通常来说,内存总线以字(word, 4byte)为最小单位进行传输。为了避免传输的数据被截断,需要重新指向来获取数据所造成的额外的开销,GCC对这个程序进行了内存对齐的处理。变量s是一个short型变量,在该环境下占2byte。假设紧随其后的double变量d在内存上紧接着s存储,当数据传输过来时,由于以字为单位进行传输,变量d将会被截断,为了恢复变量d需要做额外的处理。因此,在此处s后面,GCC填充了2byte的占位字节,使其称为一个完整的word,而相应的d则紧随其后,在两个齐整的word下存储。这就是为什么var这个结构体使用了比理想状态下更多的空间。

下面我们对于代码进行小小的调整,我们修改var所对应的结构体,将s2变量的位置提升到s之下,其余部分不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv){
int i,j,k;
double d;
float f;
void *p;

struct fred {
int a;
short s;
short s2;
double d;
double d2;
}var;

double a[30][30];
int *pd2;
double *pd;
pd2 = (int*) malloc(sizeof(int));
pd = (double*)malloc(sizeof(double));

printf("i %d %016lx\n",sizeof(i),&i);
printf("j %d %016lx\n",sizeof(j),&j);
printf("k %d %016lx\n",sizeof(k),&k);
printf("d %d %016lx\n",sizeof(d),&d);
printf("f %d %016lx\n",sizeof(f),&f);
printf("p %d %016lx\n",sizeof(p),&p);

printf("var %d %016lx\n",sizeof(var),&var);
printf("var.a %d %016lx\n",sizeof(var.a),&var.a);
printf("var.s %d %016lx\n",sizeof(var.s),&var.s);
printf("var.d %d %016lx\n",sizeof(var.d),&var.d);
printf("var.s2 %d %016lx\n",sizeof(var.s2),&var.s2);
printf("var.d2 %d %016lx\n",sizeof(var.d2),&var.d2);

printf("a %d %016lx\n",sizeof(a),a);
printf("pd %d %016lx\n",sizeof(pd),pd);
printf("pd2 %d %016lx\n",sizeof(pd2),pd2);
}

然后编译执行程序,我们得到的如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
i 4 00007fffffffdc7c
j 4 00007fffffffdc78
k 4 00007fffffffdc74
d 8 00007fffffffdc68
f 4 00007fffffffdc64
p 8 00007fffffffdc58
var 24 00007fffffffdc40
var.a 4 00007fffffffdc40
var.s 2 00007fffffffdc44
var.d 8 00007fffffffdc48
var.s2 2 00007fffffffdc46
var.d2 8 00007fffffffdc50
a 7200 00007fffffffc020
pd 8 0000000000602030
pd2 8 0000000000602010

此处我们看到,结构体对象var与我们预想的一样,占用了24byte的空间。这是因为简单的重排,使两个short变量s与s1正好占据一个word,避免了各自的对齐从而减少了空间的开销。由此可见,对于C语言中的结构体,在GCC下所有的成员变量会以对应的顺序进行存储,对于无法整齐排列在内存中的变量,会进行内存对齐,通过填充空的占位字节使其占据完整的一个word。通过对于变量顺序的调整,可以减少程序中的内存的使用!