目录
书接上文,上一章节我们对CPU读取数据的底层逻辑进行了梳理,也知道了字节对齐其实就是要把数据放到CPU“顺手的位置”,让CPU存读数据的效率更高。如果您对“为什么会有字节对齐?”感到困惑,强烈建议阅读上一章节字节对齐头疼不已?(上)---通俗易懂理清字节对齐那些事儿
结构体字节是怎么对齐的(例子一)
通过上一章节的示例分析,咱们知道了长度为4bytes的变量字节对齐的方式就是要将变量放在起始地址能被4整除的位置,从而32位CPU读写的效率最高。也可以被称为按照4字节对齐32位CPU读写的效率最高。那么在存储结构体的时候采取什么样的存储方式才能使得结构体B字节对齐,从而保证CPU在读写结构体B的每个元素时的效率都能最高呢?
还是以32位CPU的环境为例:假设咱们需要在32位CPU环境下存储定义如下的结构体B:
typedef struct {
uint16 Tinky_Winky;
uint32 Laa_Laa;
uint8 Po;
}B;
小梵的存放方式
小梵说:“简单!上节课咱们讲了存储的起始地址要放在能被4整除的位置上就OK啦!”那咱们就看看使用小梵的存放方式:将结构体B存放的起始地址选在0x8000,同时后面的数据按照顺序紧挨存放,在32位CPU读写数据时会发生什么。
(为了方便演示,同样咱们只展示数据段0x8000~0x800B)
看着不错欸,咱们先使用CPU试着读取一下元素Tinky_Winky:
由于Tinky_Winky元素的起始地址也是0x8000,能够保证CPU一次读取就能获得全部的数据,那么再试试读取元素Laa_Laa呢:
这里就出现问题了,由于元素Laa_Laa的起始地址是0x8002,Laa_Laa的数据长度又为4bytes,CPU无法一次读取就获得全部的数据,需要两个读周期分开读才能获取全部数据。先不着急,咱们再看看读取元素Po的情况:
读取元素Po还是很顺利的,能够一次读取就获得元素Po的所有数据。
总的来说,这种存放结构体的方式能够顺利读取元素Tinky_Winky和元素Po,但在读取元素Laa_Laa时由于元素Laa_Laa没有对齐,导致需要消耗两个读周期。很明显这样的存储的方式还有可以优化的空间!
小文的存放方式
小文想:“在存放结构体的时候可不可以把每个元素都放在能被4整除的位置上?这样CPU在读取元素的时候就每个元素都可以一次性读取啦!”那我们按照小文的方式存储结构体,如下图所示:
可以看到,为了使得每个元素都对齐,我们不得不在元素Laa_Laa前填充了两个字节,现在结构体B的所有元素的起始地址都能被4整除了(每个元素都按照4字节对齐),我们尝试着读取结构体B的各个元素:
这样的存储方式能够保证CPU一次读取就能获得全部的数据,那么再试试读取元素Laa_Laa呢:
通过填充字节,现在元素Laa_Laa也能被CPU用一个读周期进行读取了,咱们再看看读取元素Po的情况:
读取元素Po也还是很顺利的,能够一次读取就获得元素Po的所有数据。
果然通过填充字节的方式,达到每个元素都能满足字节对齐的要求,这样CPU在读取每个元素的都能做到只读取一次。需要注意的是填充字节是手段,目的是使得结构体的每个元素都能按照4字节对齐。但是这样做也是有代价的,代价就是数据段0x8004~0x8005被浪费掉。
所以破案啦!结构体内存对齐的本质原来是通过填充字节,使得结构体的每个元素都能够完成字节对齐,更进一步地说是“通过牺牲空间来换取时间!”。(空间又一次为了效率做出了让步,QQ空间听了泪流不止)
结构体字节是怎么对齐的(例子二)
通过上面的例子,我们似乎获得了32位CPU字节对齐的真理:“将结构体里的每个元素都放在能被4整除的地址上(每个元素都按照4字节对齐)”。那我们试着用这个方法传输一下下面这个结构体吧:
typedef struct {
uint16 Tinky_Winky;
uint16 Dipsy;
uint32 Laa_Laa;
uint8 Po;
}C;
按照之前的存储方法“将结构体里的每个元素都放在能被4整除的地址上”,存储的空间结构如下:(为了方便演示,同样咱们只展示数据段0x8000~0x800F)
看着不错欸,咱们使用CPU试着依次读取结构体C的四个元素:
由于Tinky_Winky元素的存储起始地址是0x8000,能够保证CPU一次读取就能获得全部的数据。
由于Dipsy元素的存储起始地址是0x8004,能够保证CPU一次读取就能获得全部的数据。
由于Laa_Laa元素的存储起始地址是0x8008,能够保证CPU一次读取就能获得全部的数据。
由于Po元素的存储起始地址是0x800B,能够保证CPU一次读取就能获得全部的数据。
看来这种存放结构体的方式能够保证每个元素在被CPU读写时都能效率最大化。但是这样的对齐方式还有没有优化的空间呢?
小文的存放方式
小文认真地观察了这个结构体,发现如果按照下面的这种存储方式也能保证结构体的每个元素的读写效率最大,与此同时结构体C还能占用更少的存储空间:
为了验证这种存储方式是否能保证数据读写的效率最大化,咱们试着使用CPU依次读取结构体C的四个元素:
由于Tinky_Winky元素的存储起始地址是0x8000,能够保证CPU一次读取就能获得全部的数据。
虽然Dipsy元素的存储起始地址是0x8002,并不能被4整除,但是CPU在读取0x8000~0x8003数据段时,同样能够一次性读取元素Dipsy。
由于Laa_Laa元素的存储起始地址是0x8004,能够保证CPU一次读取就能获得全部的数据
由于Po元素的存储起始地址是0x8008,能够保证CPU一次读取就能获得全部的数据。
看来这种存放结构体的方式能够保证每个元素在被CPU读写时都能效率最大化,同时还能够保证空间利用率最高。
再来看看对齐规则(核心)
通过上面两个例子,很多同学可能感觉越来越迷惑了。例子一好理解,就是在存储结构体的时候通过填充字节的方式使得CPU读写效率更高,但是例子二又告诉我们要在保证CPU读写效率的同时还要兼顾空间利用率。
通过例子总结的对齐原理:
1) 要通过填充字节的方式进行对齐使得CPU读写效率更高
2) 要在保证CPU读写效率的同时还要兼顾空间利用率
那我们在存储结构体时到底该怎么做呢?还记得“字节对齐头疼不已?(上)”开篇的对齐规则定义吗?总结的对齐原理实际上就是对“结构体对齐规则”的原理性总结,实际上咱们存储结构体的时候只要遵守“结构体对齐规则”就能保证即满足了CPU的读写效率,同时还兼顾了空间利用率!
编译器: 结构体对齐规则:
1) 数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。
2) 结构体的自身对齐值:其成员中自身对齐值最大的那个值。
3) 指定对齐值:#pragma pack (value)时的指定对齐值value。默认是4。
4) 数据成员、结构体的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。
下面咱们结合两篇博客所举的例子,讲解一下对齐规则的含义和要领
对齐规则一:
“数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。”
有没有发现对齐值等于数据类型的长度?这就对啦,对齐值其实就是存放这个数据的空间大小,比如double型长度为8字节的数据,也就以为着double型数据需要8字节长的格子才能把它放下。对齐值的长度规定是咱们存储结构体的基石,毕竟咱们如果不知道这个数据有多长咱们怎么知道该把他放在哪里CPU读取效率更高呢?
对齐值的长度我们知道了,对齐值有什么用呢?还记得之前一直讨论的要“首地址放在能被4整除的位置上”和“按照4字节对齐”吗,其中4就是我们例子中的对齐值,按照对齐值对齐说人话就是把数据放在对齐值的整数倍地址上CPU读写数据最顺手,效率最高。
对齐规则二:
“结构体的自身对齐值:其成员中自身对齐值最大的那个值。”
这一句话特别重要,还记得“结构体字节是怎么对齐的(例子一)”中我们为什么要4个4个放吗,因为4就是成员中对齐值的最大值,这样才能保证每个元素在被读写时效率都是最高的!
对齐规则三:
“指定对齐值:#pragma pack (value)时的指定对齐值value。默认是4。”
默认是4?为什么?这个默认值与CPU的位宽有关。在32位CPU数据宽度是4字节,这也就是默认是4的原因。还记得我们在上一章节举例时谈到过:“32位CPU只能4字节一取”吗?“4字节一取”的意思就是对齐值默认为4。
对齐规则四:
“数据成员、结构体的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。”
前三个规则其实都是在描述原理一:“要通过填充字节的方式进行对齐使得CPU读写效率更高”,第四个规则其实就是在描述原理二:“要在保证CPU读写效率的同时还要兼顾空间利用率”。
结合本文的示例“结构体字节是怎么对齐的(例子二)”来理解这个规则:在存储第二个元素时自身对齐值=2(第二个元素Dipsy是uint16,2字节长的数据),当前指定的pack值默认为4,根据规则“有效对齐值=min{自身对齐值,当前指定的pack值}。”有效对齐值取的更小的2,所以我们第二个元素的首地址(0x8002)并没有被4整除,而是被2整除(也就是向2对齐)的时候,也能实现字节对齐。
通过这样的规则实际上是为了达到了节省空间的目的。也就是原理二:“要在保证CPU读写效率的同时还要兼顾空间利用率”。
结构体对齐真不容易!
结构体对齐实用TIPS
在工程中如果我们不对对齐值进行定义,编译器会按照默认的对齐值进行字节对齐(32位CPU是4,64位CPU是8)。这样很可能会填充我们不想要的空字节,特别是在我们进行协议封装和拆解的时候。我们总不想在发送协议中规定的128个定长数据的时候,由于编译器的字节对齐优化导致协议封装结构体过长导致问题吧(血和泪的教训)
此时我们可以使用对齐值定义#pragma pack (1)来强迫编译器按照对齐值为1的情况进行对齐,这样就避免了空字节的填充,实际的结构体数据长度也就完全等于定义的结构体长度了。