【内存对齐】第二篇·结构体内存对齐的规律与原则

上一篇:【内存对齐】第一篇·一道 sizeof 的 面试笔试题 带来的深刻反思

第一步 提出问题

首先,我们确定我们这一阶段的目标:

  1. 结构体变量的首地址是如何得到的?
  2. 探究结构体变量的大小是如何确定的?

第二步 编程实现

为了探究上面提出的两个问题,我们先尝试将结构体定义为全局变量(局部变量可能会使问题变得复杂),通过打印 sizeof(struct) 和 &struct 的方式,得到结构体的大小和首地址。
我所使用的环境:Windows 系统,x86_64,编译器是 GCC v7.4.0。
GCC v7.4.0

示例代码如下:

#include "stdio.h"
char ph;        //Add a char placeholder to make struct start from an odd pointer value.
struct test_stt{
        char hate;
};
struct test_stt test;

int main(void)
{
        printf("Size: Char[%d], Short[%d], int[%d]\r\n", sizeof(char), sizeof(short), sizeof(int));
        printf("Sizeof struct = %d\r\n", sizeof(test));
        printf("Address of struct = %p", &test);
        return 0;
}

这里在 test 前,定义了一个 char 变量,目的是让结构体不是直接“自然对齐”,而是通过自己“调节”而实现对齐。

运行后,得到结果如下:

Size: Char[1], Short[2], int[4]
Sizeof struct = 1
Address of struct = 0x1004071a1

查看其对应的 .map 文件(暂时不需要探究):

COMMON          0x00000001004071a0        0x2 /cygdrive/c/Users/lonel/AppData/Local/Temp/ccG4DRqr.o
                0x00000001004071a0                ph
                0x00000001004071a1                test
 *fill*         0x00000001004071a2        0x2 
 COMMON         0x00000001004071a4        0x0 /usr/lib/gcc/x86_64-pc-cygwin/7.4.0/libgcc.a(_ctors.o)
                0x00000001004071a4                __bss_end__ = .

这里看到test 结构体起始地址不是从 a0 这种整整的地方开始的,这正是定义了一个全局char类型ph的作用:强行让test从 a1 这种单不楞登的地方开始,以便让我们要探究的规律更明显。

第三步 探究规律

警告:这个步骤读起来略显枯燥乏味,如果能同时做编程实现,理解起来会比较深入,但由于我们的环境不同,结果还有可能不同(后面 《第五阶段·不同环境的规律》会讲到)。
下文的代码段格式为:

  • 结构体的构成(加粗
  • 程序运行的结果(普通)。

Step1. 结构体中只有一个成员

将结构体定义中的 char,换成 short,int,分别得到 Address of struct 和 Size:

char: Address of struct = 0x1004071a1 size:1
short: Address of struct = 0x1004071a2 size:2
int: Address of struct = 0x1004071a4 size:4

  • Address 猜想:这个结构体的初始地址和结构体成员变量的类型有关。(否定了都从a1位置开始的密堆积猜想)
  • Size 猜想:这个结构体的大小和结构体成员变量的类型有关。(否定了但成员结构体都占4字节的猜想)

Step2. 结构体中有2个成员

我们每个 Step 的目的是为了验证上一阶段的猜想(如用反例反证),并提出新的更加普遍适用的猜想。

将结构体定义修改为两个成员,可以得到:

char+char: Address of struct = 0x1004071a2 size:2
short+short: Address of struct = 0x1004071a4 size:4
int+int: Address of struct = 0x1004071a8 size:8
char+short: Address of struct = 0x1004071a4 size:4
char+int: Address of struct = 0x1004071a8 size:8
short+char: Address of struct = 0x1004071a4 size:4
int+char: Address of struct = 0x1004071a8 size:8
short+int: Address of struct = 0x1004071a8 size:8
int+short: Address of struct = 0x1004071a8 size:8

根据上面的结果,有以下猜想:

  • Address猜想:结构体的初始地址和最宽的成员变量的类型有关,可能是“最宽类型宽度x成员个数”。
    但理论上不是,否则有可能有一个很大的结构体,会与前一个变量的地址偏移量很大,而这中间的空间(偏移的空间)都被浪费掉了。所以应该是有个上限的。
  • Size 猜想:结构体的大小和最宽的成员的类型有关,可能是“最宽类型的宽度*成员个数“。

Step3. 结构体中有3个及以上成员

形如

struct test_stt{
        char hate;
        char like;
        char love;
};

得到的结果:

char+char+char: Address of struct = 0x1004071a4 size:3

显然,上面的Address猜想是错误的,那么究竟是怎样的规律呢?

Step3-1. 结构体中有多个 同类型 成员

先看一下成员变量是同类型的规律:

char: Address of struct = 0x1004071a1 size:1
char+char: Address of struct = 0x1004071a2 size:2
char+char+char: Address of struct = 0x1004071a4 size:3
char * 4: Address of struct = 0x1004071a4 size:4
char * 5: Address of struct = 0x1004071a4 size:5
char * 6: Address of struct = 0x1004071a4 size:6
char * 7: Address of struct = 0x1004071a4 size:7
char * 8: Address of struct = 0x1004071a8 size:8
char * 9~15: Address of struct = 0x1004071a8 size:9~15
char * 16: Address of struct = 0x1004071b0 size:16
short * 1: Address of struct = 0x1004071a2 size:2
short * 2: Address of struct = 0x1004071a4 size:4
short * 3: Address of struct = 0x1004071a4 size:6
short * 4: Address of struct = 0x1004071a8 size:8
short * 5~7: Address of struct = 0x1004071a8 size:10~14
short * 8: Address of struct = 0x1004071b0 size:16
short * 9~15: Address of struct = 0x1004071b0 size:18~30
short * 16: Address of struct = 0x1004071c0 size:32
int * 1: Address of struct = 0x1004071a4 size:4
int * 2: Address of struct = 0x1004071a8 size:8
int * 3: Address of struct = 0x1004071a8 size:12
int * 4: Address of struct = 0x1004071b0 size:16
int * 5~7: Address of struct = 0x1004071b0 size:20~28
int * 8: Address of struct = 0x1004071c0 size:32

Step3-2. 结构体中有多个 不同类型 成员

再看一下成员变量是不同类型的规律,搞一个3种基本类型成员变量都有的结构体:

struct test_stt{
        char hate;
        short like;
        int love;
};

不同的排列与组合,可以得到:

char+short+int: Address of struct = 0x1004071a8 size:8
char+int+short: Address of struct = 0x1004071a8 size:12
short+char+int: Address of struct = 0x1004071a8 size:8
short+int+char: Address of struct = 0x1004071a8 size:12
int+char+short: Address of struct = 0x1004071a8 size:8
int+short+char: Address of struct = 0x1004071a8 size:8

上述3种基本类型排列组合的 6 个例子中,可以发现当 short 和 char 在一起的时候,size 就是 8,即两个 int 的宽度;当分开的时候,就是 12,即 3 个int 的宽度。同时也能发现结构体末尾的成员会按照结构体的最大宽度成员自动补齐。可以猜测在内存中是这样排布的:
short+char+int
short+int+char
但是 char+short+int是如何排布的呢?是“密集排布”(如下图)
密集排布
还是“对齐排布”(如下图)
对齐排布
呢?

下面我们通过编程测试来验证:如果是“密集排布”,应有sizeof(char+short+char+int) = 8(如下图)
char+short+char+int密集
如果是“对齐排布“,应有sizeof(char+short+char+int) = 12(如下图)
在这里插入图片描述
实际测试下来:

char+short+char+int: Address of struct = 0x1004071a8 size:12

也就是说,应该是“对齐排布”。

接下来,再搞一些自由组合,增加样本容量:

int+int+char+int: Address of struct = 0x1004071b0 size:16
int+int+short+int: Address of struct = 0x1004071b0 size:16
int+int+short+char: Address of struct = 0x1004071a8 size:12
char+short+char+int: Address of struct = 0x1004071a8 size:12
char+int+short+int: Address of struct = 0x1004071b0 size:16
int+int+short+int+int*4+…: Address of struct = 0x1004071c0 size:>=32
short*16+…: Address of struct = 0x1004071c0 size:>=32

到这里,可以提出新的猜想了。

  • Address 猜想:
    • 结构体首地址只和size有关。
    • 当 结构体大小 <= 0x04 时,起始地址是 2^( 大于等于log2(size) 的最小整数)的整数倍。(如 size = 3, 大于等于 log2(size) 的最小整数 = 2,起始地址是 2 ^ 2 = 4 = 0x4 的整数倍;其他 size 读者可以自己算一下)
    • 当 0x04 <= 结构体大小 <= 0x10 时,起始地址是 2^(小于等于log2(size)的最大整数)的整数倍。(如 size = 8,小于等于 log2(size) 的最大整数 = 3,起始地址是 2 ^ 3 = 8 = 0x8 的整数倍;其他 size 读者可以自己算一下)
    • 当 0x10 <= 结构体大小 <= 0x20 时,起始地址是 (0x10*(size/16)) 的整数倍。(如 size = 18,size/16 = 1, 0x101 = 0x10;其他 size 读者可以自己算一下)*
    • 当 结构体大小 >= 0x20时,起始地址是 0x20 的整数倍。(参考size >=32)
  • Size 猜想:结构体内部排布方式是“对齐排布”,即 每个成员的地址与结构体的首地址之间的偏移量是该成员大小的整数倍。结构体的末尾要按照最大成员的宽度进行补齐。进而得到整个结构体的大小。

第四步 得到结论

这里再次对第三步中最后的猜想进行总结,作为一个阶段性的结论。

  • Address猜想:
    • 结构体首地址只和size有关。
    • 当 结构体大小 <= 0x04 时,起始地址是 2^( 大于等于log2(size) 的最小整数)的整数倍。(如 size = 3, 大于等于 log2(size) 的最小整数 = 2,起始地址是 2 ^ 2 = 4 = 0x4 的整数倍;其他 size 读者可以自己算一下)
    • 当 0x04 <= 结构体大小 <= 0x10 时,起始地址是 2^(小于等于log2(size)的最大整数)的整数倍。(如 size = 8,小于等于 log2(size) 的最大整数 = 3,起始地址是 2 ^ 3 = 8 = 0x8 的整数倍;其他 size 读者可以自己算一下)
    • 当 0x10 <= 结构体大小 <= 0x20 时,起始地址是 (0x10*(size/16)) 的整数倍。(如 size = 18,size/16 = 1, 0x101 = 0x10;其他 size 读者可以自己算一下)*
    • 当 结构体大小 >= 0x20时,起始地址是 0x20 的整数倍。(参考size >=32)
  • Size 猜想:结构体内部排布方式是“对齐排布”,即 每个成员的地址与结构体的首地址之间的偏移量是该成员大小的整数倍。结构体的末尾要按照最大成员的宽度进行补齐。进而得到整个结构体的大小。

第五步 求证与验证

这一步是通过各种手段对之前的结论做 Verification。(但于“深度探究”而言,还有积累素材和提出新的问题的作用,读者不用考虑)

  • 最易行的是上网随便找一篇短小的讲解对齐的文章,对于对齐整个的概念有个大概的了解,比如 Baidu百科。(很不幸的是,我发现网络上讲解深入的文章有,但是总结出这种非常落地的、定量规律的几乎没有,可能是因为这个规律和平台有关,后面会在 《第五阶段 不同环境的影响》中介绍。但好在,我们还有其他的方法)
  • 我找了我的技术领导,简单探讨了这个问题,他 Diss 我花大把时间做这么细枝末节的探究,同时,也为我拓展了这个知识的一些其他维度,后文会涉及。
  • 我找到了我技术上的好友,非常不情愿地被我拉着,在他的电脑上跑我写的程序来验证这个问题,然后告诉我,我们的结果竟然不一样!致使他也产生了兴趣,和我一起探究。

连载中… by 2024/05/19

下一篇:【内存对齐】第三篇·显式干预对齐的三种方法

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值