c语言强制4字节对齐,C语言字节对齐4

本文详细探讨了C语言中结构体的字节对齐规则,通过实例展示了如何使用`__packed`、`__attribute__((packed))`、`#pragma pack`等方法控制字节对齐,并分析了不同字节对齐方式对内存布局和性能的影响。非字节对齐访问在某些处理器上可能会影响速度和原子操作的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

非字节对齐类型的字节对齐规则

我们可以使用“__packed”、“__attribute__((packed))”、“#pragma”等方式控制结构体的字节对齐,这些结构体的内部结构在定义时就已经确定了,当它们被包含在其它结构体内部时它们被当做一个整体来看待,其内部结构不受外面字节对齐控制方式的影响,但它们的对齐方式却受外面结构体的对齐方式控制,来看下面的例子:

#pragmapack(2)

typedef struct example26_1

{

char a;

int b;

char c;

}EXAMPLE26_1;

#pragma pack(1)

typedef struct example26_2

{

char a;

EXAMPLE26_1 b;

int c;

}EXAMPLE26_2;

#pragma pack()

typedef struct example26_3

{

char a;

EXAMPLE26_1 b;

int c;

}EXAMPLE26_3;

EXAMPLE26_1的内存分布示意图如下:

a

b

b

b

b

c

EXAMPLE26_1的数据如下:

sizeof(EXAMPLE26_1)

8

OFFSET(EXAMPLE26_1, a)

0

OFFSET(EXAMPLE26_1, b)

2

OFFSET(EXAMPLE26_1, c)

6

EXAMPLE26_1结构体按照2字节对齐,这个没什么好说的了,前面已经介绍过。

EXAMPLE26_2结构体按照1字节对齐,它里面的a、b、c都按照1字节对齐。其中b是一个按照2字节对齐的EXAMPLE26_1结构体,内部有2个填充的1字节,当b出现在要求1字节对齐的EXAMPLE26_2结构体中,b需要按照1字节对齐,注意,但其内部结构不能发生变化,那2个填充的1字节仍保留。

来看内存分布示意图:

a

b.a

b.b

b.b

b.b

b.b

b.c

c

c

c

c

EXAMPLE26_2的数据如下:

sizeof(EXAMPLE26_2)

13

OFFSET(EXAMPLE26_2, a)

0

OFFSET(EXAMPLE26_2, b.a)

1

OFFSET(EXAMPLE26_2, b.b)

3

OFFSET(EXAMPLE26_2, b.c)

7

OFFSET(EXAMPLE26_2, c)

9

EXAMPLE26_3结构体按照4字节对齐,EXAMPLE26_2结构体是按照2字节对齐的,因此b.a虽然是char型变量,但也需要对齐到2字节,在a之后需要保留一个填充字节。b.a与b.b之间保留的一个字节不是因为b.b需要对齐到4字节而保留的,而是EXAMPLE26_2结构体在定义时按2字节对齐而保留的。

来看内存分布示意图:

a

b.a

b.b

b.b

b.b

b.b

b.c

c

c

c

c

EXAMPLE26_3的数据如下:

sizeof(EXAMPLE26_3)

16

OFFSET(EXAMPLE26_3, a)

0

OFFSET(EXAMPLE26_3, b.a)

2

OFFSET(EXAMPLE26_3, b.b)

4

OFFSET(EXAMPLE26_3, b.c)

8

OFFSET(EXAMPLE26_3, c)

12

总结一下字节对齐的规则:

1.确定结构体中每种结构对齐的字节数,找出其中最大的字节对齐数N,求得结构体对齐规则的对齐数M,取M与N中的最小值min(M, N)作为该结构体的字节对齐数。结构体中每种结构的对齐数为默认对齐数P与min(M, N)的最小值min(P, min(M, N))。若结构体中包含子结构体,则先确定子结构的字节对齐数。

2.结构体中每个结构的开始都需要对齐到min(P, min(M, N))字节,若无法对齐前面会有保留的填充字节。结构体中每个结构的结束都需要对齐到下个对齐字节min(P, min(M, N)),若无法对齐则在后面填充空闲字节。

3.结构体作为一个整体存在,对于包含它的结构体来说它是一个黑盒。其内部按自己的对齐方式对齐,被包含时整体按照父结构对齐。

下面我们使用上面的规则来分析一下结构体EXAMPLE27_3的对齐方式。

#pragmapack(1)

typedef struct example27_1

{

char a;

short b;

}EXAMPLE27_1;

#pragma pack(2)

typedef struct example27_2

{

EXAMPLE27_1 a;

int b;

char c;

}EXAMPLE27_2;

#pragma pack()

typedef struct example27_3

{

char a;

EXAMPLE27_2 b;

}EXAMPLE27_3;

结构体EXAMPLE27_3中包含结构体EXAMPLE27_2,结构体EXAMPLE27_2中包含结构体EXAMPLE27_1,需要先确定结构体EXAMPLE27_1的对齐字节数。

结构体EXAMPLE27_1里面都是基本类型的变量,char型变量a对齐到1字节,short型变量b对齐到2字节,这其中最大的是2字节对齐。结构体EXAMPLE27_1使用的对齐规则是1个字节对齐,因此结构体1的字节对齐数是min(2, 1),是1字节对齐。因此,变量a对齐到min(1 ,1)=1字节,变量b对齐到min(2, 1)=1字节。

它的内存分布示意图如下:

a

b

b

EXAMPLE27_1的数据如下:

sizeof(EXAMPLE27_1)

3

OFFSET(EXAMPLE27_1, a)

0

OFFSET(EXAMPLE27_1, b)

1

结构体EXAMPLE27_2中包含了EXAMPLE27_1型的变量a、int型的变量b和char型的变量c,EXAMPLE27_1型是1字节对齐,int型是4字节对齐,char型是1字节对齐,这其中最大的是4字节对齐。结构体EXAMPLE27_2型的字节对齐规则是2字节对齐,因此结构体EXAMPLE27_2的字节对齐数是min(2, 4),是2字节对齐。因此,变量a对齐到min(1, 2)=1字节,变量b对齐到min(4, 2)=2字节,变量c对齐到min(2, 2)=2字节。变量a占用3个字节,因此在变量a之后需要有一个保留字节,变量b才能对齐到2字节,变量c之后需要保留1个空闲字节才能对齐到下一个2字节。

它的内存分布示意图如下:

a.a

a.b

a.b

b

b

b

b

c

EXAMPLE27_2的数据如下:

sizeof(EXAMPLE27_2)

10

OFFSET(EXAMPLE27_2, a.a)

0

OFFSET(EXAMPLE27_2, a.b)

1

OFFSET(EXAMPLE27_2, b)

4

OFFSET(EXAMPLE27_2, c)

8

结构体EXAMPLE27_3中包含了char型的变量a和EXAMPLE27_2型的变量b,char型是1字节对齐,EXAMPLE27_2型是2字节对齐,这其中最大的是2字节对齐。结构体EXAMPLE27_3型的字节对齐规则是4字节对齐,因此结构体EXAMPLE27_3的字节对齐数是min(4, 2),是2字节对齐。因此变量a对齐到min(1, 2)=1字节,变量b对齐到min(2, 4)=2字节。变量a占用了1个字节,因此变量a之后需要哟袷保留字节,变量b才能对齐到2字节。

它的内存分布示意图如下:

a

b.a.a

b.a.b

b.a.b

b.b

b.b

b.b

b.b

b.c

EXAMPLE27_3的数据如下:

sizeof(EXAMPLE27_3)

12

OFFSET(EXAMPLE27_3, a)

0

OFFSET(EXAMPLE27_3, b.a.a)

2

OFFSET(EXAMPLE27_3, b.a.b)

3

OFFSET(EXAMPLE27_3, b.b)

6

OFFSET(EXAMPLE27_3, b.c)

10

非字节对齐的影响

u速度影响

非字节对齐访问会比字节对齐访问花费更多的硬件访问周期,因此前者的速度也会慢一些,但处理器的设计千差万别,架构层出不穷,不同处理器非字节对齐表现出的性能也不尽相同。

下面是我测试的一组数据,测试中使用4字节对齐的指针访问非4字节对齐的地址:

ARM7TDMI

(ARMv4T)

Cortex-M3

(ARMv7-M)

Pentium(R)

Dual-Core CPU E6300

是否支持硬件对齐

硬件字节对齐访问时间

19秒

19秒

23秒

硬件非字节对齐访问时间

63秒

26秒

26秒

非字节访问效率

(字节对齐 /非字节对齐)

0.3

0.73

0.88

注:上述3种处理器的测试数据不同,不能横向比较不同处理器的访问时间,只能纵向比较同一处理器的访问时间。

从测试数据可以看出硬件非字节对齐访问确实要比硬件对齐访问速度要慢,但在不同的处理器架构上表现出的差异也是不同的。

ARM7TDMI处理器不支持硬件非字节对齐访问,需要由软件指令实现硬件非字节对齐访问,只能使用在“非字节对齐的方法”一节中的方法,将1次4字节非字节对齐硬件访问打碎成4次1字节的硬件访问,因此与软件对应的硬件指令周期数成倍增加,这也就造成了该种处理器非字节对齐访问效率是如此之低,只有0.3,几乎达到四分之一的0.25。

Cortex-M3处理器支持硬件非对齐访问,可以由一条软件指令实现1次4字节的非字节对齐硬件访问,至于硬件非字节对齐的处理部分则由硬件内部电路实现,这虽然要比硬件字节对齐访问花费更长的时间,但由于是硬件内部自动完成的,因此要比使用多条软件指令驱动硬件去完成要节省很多时间,因此效率也有了提升,达到0.73。

Intel的X86处理器E6300也支持硬件非字节对齐访问,它的非字节对齐访问效率更高,达到0.88。前2种处理器属于低端领域的处理器,而E6300则属于高端处理器,这也许是它效率最高的原因。(前2种处理器没有cache,E6300有cache,但在测试中我应该避开了cache)

u原子操作(atomic operation)影响

对于单核处理器或者是处理器的一个内核来说,硬件指令是串行执行的,从软件层次来看,它是“不能被进一步分割的最小粒子”,因此一条硬件指令不会被多线程或中断所打断,这就是原子操作。

对于不支持硬件非对齐访问的处理器若实现硬件非对齐访问就需要由多条硬件指令完成,比如说下面这个例子:

__packed int* p;

p = (int*)0x1001;

*p = 0x12345678;

为了使用4字节对齐的指针p实现对非4字节对齐的0x1001地址的访问使用了__packed,这样就将本可以使用一条硬件指令将4字节0x12345678一次写入0x1001~0x1004地址的这条指令拆分成4条对1字节访问的硬件指令,将数据0x78、0x56、0x34、0x12分别写入到0x1001、0x1002、0x1003、0x1004地址内,如果在2个线程中都有这种操作那么就破坏了原子操作,可能就会出问题。

比如说一个计数值被存储在0x1001~0x1004这4个字节里,一个线程thread_add对这个计数值进行计数自加,另一个线程thread_read读取这个计数值。

thread_add代码如下:

__packed int* p= (int*) 0x1001;

*p++;

thread_read代码如下:

__packed int* p= (int*)0x1001;

int read;

read = *p;

如果当前的计数值是0x123456FF,thread_add线程则需要将其自加到0x12345700,thread_add线程使用4次字节读取将数据0x123456FF从内存中读取到内部寄存器中,并进行自加,变成了0x12345700。然后需要再使用4次字节写入将数据0x12345700写入到0x1001~0x1004中,thread_add线程先将0x00写入到0x1001中,如果这时候发生了线程切换,切换到了thread_read线程,那么thread_read线程将从地址0x1001~0x1004中读取计数值,但此时0x1001中的数值已经被thread_add线程改写为0x00,而0x1002~ 0x1004内的数值仍为原值,因此thread_read线程读取到的数据为0x12345600,这就出错了。

对于支持硬件非字节对齐访问的处理器则不会有该问题存在,因为这种处理器的硬件非字节对齐访问是由硬件内部完成的,是一个原子操作,不会被线程打断,因此不会出错。

这篇文档拖拖拉拉的写了4个多月,为了能说的更清楚让大家看的更明白,真的费了不少力气,挺不容易的。其中有些内容涉及到处理器内部机制,处理器架构又千差万别,我没有能力找到一个全面的权威说明,因此错误也许在所难免。如有问题请到我的博客反馈,我将尽力修正blog.sina.com.cn/ifreecoding

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值