加padding的条件c语言,data alignment 和 data padding

开门见山,下面代码占据了几个字节:

struct foo {

char a;

int b;

}

最开始在不懂的时候也以为是5字节,但是如果用sizeof(struct foo)会发现输出结果是8。因为在char a后面编译器为我们填充了一些的内容。使得int是四字节的对齐的。首先先来明确的描述一下什么是对齐。

alignment

现代的CPU从内存中读取数据的时候,如果数据地址和它本身的大小是对齐的(naturally aligned),那么CPU有更好的效率。比如说一个四字节的数据,如果他的地址是四字节对齐的,那么效率更好。至于为什么是这样,暂时没有找到一个官方的说法。我不知道为什么会这样,查了不少资料没看到确切的说法,但是这里只是描述一下这个现象。

下面是各个数据的naturally aligned:摘自 wikipedia

7ba6c2c5acfa

naturally aligned

问题阐述

CPU在访问内存中的数据的时候总是word size的。比如说32bit的数据总线。那么每个周期获得的数据就是32 bit。如果数据类型小于word size那么,该数据总是能在一个周期内读取到。如果一个数据的低地址部分在这个word,另外一部分在高地址部分的word。那么获得这个数据的时候就需要额外的操作来这两个word的数据中不需要的部分去除,然后再拼接为一个所需的部分,导致性能变差。如下图:

7ba6c2c5acfa

http://www.songho.ca/misc/alignment/dataalign.html

为了避免上述操作,有些CPU会采取一些方法来避免访问非对齐的数据。比如说ARM架构,在ARMv6 ISA之前的CPU强制所有多字节加载和写入指令需要对齐。比如说mov dword(举个例子,我不知道ARM汇编语法)就一定需要对齐。有些CPU会将非对齐的地址通过向下取整到一个对齐的地址,或者有些CPU会直接抛出异常。

数据对齐的一些例子:

在C语言中声明几个变量:

int c =1;

int *p = &c;

short x = 2;

int main() {

return 0;

}

在我的电脑上的输出结果是:

0x404078

0x404080

0x404088

很容易计算出来,int c是四字节对齐, int *p是八字节对齐,short x = 2 是2字节对齐。效果图如下所示。结果表明符合和前面所说的,和数据本身的大小对齐,这样带来的性能更好。这也就说明了,在代码当中,连续申请的变量在内存中并不是连续的。

data padding

同样的问题在结构体当中又会有些稍微的不同。其实,在CPU看来,结构体和单独的变量并没有什么不同。编译器在编译的时候都会在在两个变量之间插入一些字节,让这些变量都是对齐的。还是拿前面的例子

struct foo {

char a;

int b;

}

用sizeof来得到这个结构体的大小,结果是8,而不是5。原因是在char a和int b之间有三个字节的padding。再来看一个例子。

struct foo {

short a;

short b;

short c;

}

这里是否有padding呢?答案是没有的,一个结构体的地址,就是它第一个变量的地址。所以在分配地址的时候,short a是对齐的,那么很自然的short b也是对齐的。short c与此类似。

再来看一个例子。

struct foo {

short s;

char n[3];

}

这个结构体的大小是多少字节呢? 可能会认为是5。因为此时short在最开始的地方。所以此时short不需要对齐。因为char的长度只有一个字节。所以它不需要对齐。不过,如果用sizeof来得到这个结构体的大小,输出是6。在思考这个之前,来看另外一段代码,假设我们以上这个结构体定义的数组:

struct foo {

short s;

char n[3];

}

struct foo bar[2];

比如说,bar[0]的地址是2(必定是2字节对齐的,因为要保证short 是2字节对齐的),那么short s的地址就范围就是2,2+1这两个字节,char n[3]的起始地址就是2+2直到2+4。然后bar[1]的地址是2+5开始。实际上这就错了,为了保证bar[1]中的short也是对齐的。编译器会在char n[3]后面插入一个字节的内容。所以sizeof 得到的大小就是6字节,而不是5字节。

P.S.

对于结构体来说,结构体占据多少字节的计算非常简单,结构体的字节大小等于它最大的成员变量的整数倍。比如说结构体中有一个long,那么结构体的大小肯定是8的倍数。

再来看一个稍微复杂一些的例子。如果在结构体之类又定义了一个结构体,结果又是如何?首先要保证内部的是对齐的,然后还需要确保外部的不会破坏内部的对齐。

struct foo{

char a;

struct bar {

char b;

int *p;

}

}

先来看内部的struct bar这个结构,为了保证int *p对齐,那么char b 之后插入7字节的空白数据为了不破坏内部的对齐结构,从而也要在char a之后插入7字节的空白数据。所以sizeof得到的大小就是24字节。

如何取消padding

有些时候,我们不需要填充。因为硬件的一些要求,对数据结构的字节数有要求的,填充就不匹配了。在GNU GCC中,使用attribute((packed))可以取消填充。或者在结构体定义之间加上#pargma pack(1)也行

strcu foo {

char a;

long b;

} __attribute__((packed))

#pargma pack(1)

strcut foo {

char a;

long b;

}

对于这个结构,sizeof()得到的大小就是5字节。

我们能如何来避免padding吗

为了对齐插入的padding会增大一个结构体的大小。从而也会导致在内存中空间的浪费,那么我们有什么办法来避免这个padding呢? 答案也非常简单,我们在定义的时候,尽量让大的元素在前面,小的元素在后面。一来就可以避免padding,比如下面的例子。

struct Foo {

long b;

short b;

char c;

}

如果是这样,就不会存在padding,sizeof得到的大小就是16字节。如果小的在前,比如说 char c,long a, short b;这样的顺序。此时的大小就变成了24字节。

一些问题

如果数据类型小于word size,那么是否可以不对齐呢? 因为这样也可以一个周期就读取到数据。

根据这篇文章,答案是可以的。虽然说现实中,CPU还是将小于word size的数据对齐。至于为什么试想一下,如果word size是32 bit。一个short 可以放在一个word size内的好几个地方,如下图蓝色,但是不能放在橙色区域,这样就到下一个word去了。

7ba6c2c5acfa

image.png

这样的话其实奇地址看起来也没问题。但是。难道在设计芯片的时候,我们要去额外判断,这个类型放置在这个地址是否再+它的大小是否会超过一个word?这明显没必要,如果我们只要做到naturally aligend,那么我们一定可以保证它肯定不会被分成两半。所以, 没必要。

如果说CPU每次读取的数据都是word size的,那是否就说明低几位的地址线没什么用了?

不是的。以8086为例子,8086的数据总线是16bit的。8086总是从偶地址开始读取。一个word总的低字节放到数据先的0-7位,高字节放到0-8位。但是8086也支持从内存中读取单个字节的数据。为了完成这个工作,A0地址线和BHE引脚共同用于决定当前读取的是单字节还是一个word。如下图:

7ba6c2c5acfa

8086

这篇解释了8086是如何从从不同的地址来读取不同的尺寸的数据。虽然再stackexchange.com里面的回答里面看起来好像A0-A19都是用于8086地址线的。不过根据8086的一份文档提到:

Physically, the memory is organized as a high bank (D15–D8) and a low bank (D7–D0) of 512K 8-bit bytes addressed in parallel by the processor’s address lines A19–A1. Byte data with even addresses is transferred on the D7–D0 bus lines while odd addressed byte data (A0 HIGH) is transferred on the D15–D8 bus lines. The processor provides two enable signals, BHE and A0, to selectively allow reading from or writing into either an odd byte location, even byte location, or both. The instruction stream is fetched from memory as words and is addressed internally by the processor to the byte level as necessary

A1-A19是用于地址线的,而A0和BHE是两个enable signals。所以,并不是说低位的地址线是没用的。

那么在现代的CPU情况又是如何呢?

根据这篇文章,8086将地址分为odd bank和even bank。odd bank里面放的是奇地址的字节,连接到地址线的D15-D8,even bank里面放的是偶地址的字节,连接到地址线的D7-D0。一个word分别从odd bank和even bank各拿一个字节。如下图:

7ba6c2c5acfa

memory bank

情况到了32bit的机器,稍微有些不一样,但是基本的逻辑差不多。在x86的32bit机器下,就变为如下这样的:

7ba6c2c5acfa

memory bank

这样的CPU之下,地址应该要四字节对齐。所以最低的两位应该用于byte enable了,就如同前面8086中的A0和BHE一样。在这里,应该是A0,A1,BHE。我猜想的,没有找到相关资料。

现代CPU还需要对齐吗?

根据这篇文档,现代的CPU可以支持对齐和非对齐的访问,且没有性能的差异。但是他提到树莓派里边还是影响的。我也在我们的电脑上做了实验,我的电脑为i5-9400 2.9Ghz。

代码如下,分别运行了对齐以及非对齐的情况,发现两者时间差不多。代码如下:

int main() {

struct Foo {

char x;

short a;

long z;

} __attribute__((packed));

struct Foo foo = {1,2,3};

double used_time;

clock_t start,end;

start = clock();

for (int i = 0; i < 1e7; ++i) {

foo.z++;

}

end = clock();

used_time = ((double)end-start) / CLOCKS_PER_SEC;

printf("used time:%lf",used_time);

}

因为编译链接的地址都是虚拟地址,那么在物理地址的情况下能否保证对齐呢?

这个疑问来源于,会不会在虚拟地址当中是对齐的。当经过页转为实际的物理地址以后,能否保证原来的地址还是对齐的。我认为是可以的,原因是一个地址是否对齐,主要看的是这个地址的低位是否有效。比如说八字节对齐,那么地址只能是以0或者,8结尾的。比如说0x12340,0x12348。无论在哪个页,低位的地址不会收到页转为物理地址的影响。所以我认为还是保证对其的。

References

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值