当前位置:首页 > 内存 > 正文

内存对齐规则

  • 内存
  • 2024-05-08 00:06:55
  • 4692

一、Go语言中恰到好处的内存对齐

在开始之前,我希望你能计算一下part1占用的总大小。

输出结果:

经计算,Part1结构体占用内存大小为1+4+1+8+1=15字节。我想有些朋友是这样算的,似乎并没有什么问题。

实际情况是怎样的呢?我们看一下实际调用如下:

输出结果:

最终输出占用32字节。这与之前预想的结果完全不同。这充分证明了之前的计算方法是错误的。为什么?

这里需要提到“内存对齐”的概念,以便我们能够以正确的姿势进行计算。接下来详细说一下它是什么。

有些朋友认为读取内存只是字节数组的简单排列。

上图展示了一个陷阱和胡萝卜读取记忆的方法。然而CPU实际上并不是逐字节读写内存的。相反,CPU是逐块读取内存的,块大小可以是2、4、6、8、16字节等。我们将块大小称为内存访问的粒度。如下所示:

示例假设访问粒度为4。CPU以每4字节的访问粒度读写内存。这才是正确的态度

另外,作为一名工程师,这个知识点你是有必要学习的:)

上图中,假设阅读是从Index1、Es会给出崩溃问题。因为它的内存访问边界没有对齐。所以CPU会做额外的处理工作。如下:

从上面的过程可以得出,不做“内存对齐”有点“繁琐”。由于这样增加了很多耗时的动作

假设完成了内存对齐,从index0读取了4个字节,那么只需要读取一次,不需要额外的操作。这显然更加高效,是一种以空间换时间的标准方式

不同平台上的编译器都有自己默认的“对齐系数”,可以通过#pragmapack(n)预编译命令进行更改。n指“对准系数”。一般来说,我们常用的平台的系数如下:

另外,需要注意的是,不同的硬件平台所占用的尺寸和方向值可能会有所不同。因此,本文中的数值并不明确。调试时需要考虑机器的实际情况。

输出结果:

在Go中,可以不安全地调用.Alignof返回对应类型的对齐情况。通过观察输出结果可以看到,基本都是2^n,最大不会超过8。这是因为我的笔记本电脑编译器(64位)的默认对齐因子是8,所以最大值不会超过这个数字。

上一节提到了结构体中的成员变量必须是面向字节的。因此,当然,作为最终结果的结构也必须是面向字节的。

接下来,我们分析“它”经历了什么,影响了“预期”的结果

在每个之后member变量是对齐的。根据规则2,整个结构体本身也一定是面向字节的,因为可以发现它可能不是2^n,也不是偶数倍。显然不符合对齐规则

根据规则2,可以得出对齐值为8。偏移量现在为25,它不是8的倍数。所以偏移量为32。对齐结构体

Part1内存布局:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx

通过本节的分析我们可以看到前面的“为什么”是“计算”错了吗?

这是因为实际的内存管理并不是基于“一根胡萝卜一个坑”的想法。但一点一点地。这篇读写文章是通过用空间换取时间(效率)的思想来完成的。另外,还需要考虑到不同平台的内存操作。

上一节可以看出,结构体的内存根据成员的类型进行对齐等动作变量。那么如果我们假设场序不同的话,会不会有什么变化呢?让我们一起尝试一下:-)

输出结果:

根据结果,我们会惊讶地发现,即使是成员变量字段顺序的“简单”更改也会改变Body结构占用尺寸

接下来我们一起来分析part2,看看它的内部和之前有什么不同导致了这样的结果?

符合规则2,无需额外对齐

Part2内存布局:ecax|bbbb|dddd|dddd

通过比较part1和part的内存布局2:你会发现两者有很大的区别。如下:

仔细一看,Part1中有很多padding。显然它占用了大量空间。内饰是什么样子的?

通过本文的介绍,我们可以看到,不同类型需要字节对齐来保证内存访问边界

就不难理解为什么结构体要调整字段顺序了成员变量的数量可以减少结构的大小,因为它巧妙地减少了填充的存在。让它们更加“紧凑”。这对于加深Go的内存布局印象以及优化大对象非常有帮助


二、内存对齐原理

对于程序来说,如果变量的数据存储范围在寻址步长范围内,则可以一次寻址读取变量值,如果数据存储范围超出步长范围,则需要读取两次地址,然后再读取连接数据,大大降低了效率。例如,double类型数据在内存中占用8个字节,如果地址是8,那么就很容易处理,如果是20,那么就需要两次寻址。这就创建了一种数据对齐规则,就是尽可能将数据存储在一个步骤中,避免跨步骤存储,这就是内存对齐。在32位编译环境中默认对齐方式为4字节,在64位编译环境中默认对齐方式为8字节对齐。

现代处理器一般都有多级缓存,处理器访问这个缓存中的数据的效率比访问内存中的数据要高得多(就像处理器访问内存中的数据一样)数据量很大。比访问磁盘上的数据更有效)。
如上所述,一般情况下,CPU总是以字大小(在32位处理器上通常为4字节)来访问数据,因此如果数据没有与内存对齐,那么当CPU访问该数据时,可能会出现错误。需要执行更多的读取操作。在这样的机器上,读取2个字节的数据往往比读取4个字节的数据慢。

增强的访问范围
对于任何给定的地址空间,如果一个架构可以确定2LSB始终为0(如32位机器),那么它可以访问4倍以上的内存(2位可以代表4种不同的状态)。从地址中删除2个LSB会导致4字节内存对齐或“步进”,因为地址每次增加1时,实际上会增加bit2,而不是bit0。(因为低2位始终为00)
这甚至会影响系统的物理设:如果地址总线需要少2位,则CPU上可能会少2个引脚。

前面说过,CPU每次访问数据的宽度是一个字,如果C语言程序中的数据总是与内存对齐,那么CPU对数据的访问总是原子的,即。对于许多没有键的数据来说,问题正确的结构操作和其他并发要求至关重要。

规则:
1数据成员对齐规则:对于结构(或联合)数据成员,第一个数据成员放置在偏移量0处,后续的每个数据成员放置在偏移量0处。每个数据成员根据#pragmapack指定的值和数据成员本身的长度之间较小的值进行对齐。
2.整个结构体(或联合体)的对齐规则:数据成员完成各自的对齐后,结构体(或联合体)本身也必须根据#指定的值进行对齐。pragmapack和最大结构(或联合)使用的数据成员的长度越短。
3.结合1和2可以得出结论:当#pragmapack的n值等于或超过所有数据成员的长度时,n值的大小将不起作用。

#pragmapack实际上指定了内存对齐因子,如1,2,4,8,16。xcode的默认对齐因子是8。

求出内存的字节大小下面两个结构体

打印结果

分析如下
第一个DemoStruct1,初始偏移量为0
a,类型为int,4字节,<8,与4对齐,存储位置为[0,3]
b,int类型,4字节,<8,与4对齐,存储位置为[4,7]
c,类型字符,1字节,<8,按1对齐,存储位置为[8,8]
d,double类型,8字节,=8,按8对齐,当前位置不够,补齐先[9,15],然后保存,存储位置为[16,23]
e[7],数组类型char,7个字符字节,<8,1对齐,存储位置为[24,30]
最后填写8的整数倍,即[31,31]
综合:使用位置为[0,31],占用字节数为32

重新分析demoStruct2,起始偏移量为0
a,类型int,4字节,<8,与4对齐,存储位置为[0,3]
b,类型int,4字节,<8,与4对齐,存储位置为[4,7]
c、字符类型,1字除法,<8,按1对齐,存储位置为[8,8]
e[7],字符数组类型,7个字符字节,<8,按1对齐,存储位置为[9,15]d,double类型,8个字节,=8,按8对齐,存储位置为[16,23]
综合:使用位置为[0,23],占用字节数为24

调整合适的变量类型或位置,有助于提高内存使用率