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

内存命令对齐

  • 内存
  • 2024-05-23 12:20:39
  • 440

一、内存对齐问题

1.平台原因(植入原因):不是所有的硬件平台都可以访问任意地址的任意数据;有些硬件平台只能在某些地址获取某些类型的数据
,否则会出现硬件异常。
2.性能原因:数据结构应尽可能与自然边界对齐。原因是为了访问未对齐的内存,处理器需要执行两次内存访问;访问对齐的内存只需要一次访问。(如果对齐,CPU不需要跨越两个操作字。如果没有对齐,则需要访问两个操作字来连接所需的内存地址)

指针的大小一般是距离机器的一个维度

通过Go语言的structlayout工具,可以得到下图

这些类型之前已经介绍过了,这里先介绍一下地图和接口,也特意强调一下makehmap函数返回一个指针,因此映射对齐是一个机器字。

回顾用于复制保护的结构体字段空也是不可避免的。放在首位,案件得到了解决。

计算机架构可能需要内存地址对齐;意味着变量的地址是因子的倍数,意味着变量的类型是对齐值。
Alignof函数接受表示任何类型变量的表达式作为参数,并返回该变量(类型)的对齐值(以字节为单位)。对于变量x:

这是因为int64在bool之后没有对齐。
它是32位对齐的,但不是64位对齐的,因为们使用的是32位系统,所以实际上只有两个彼此相邻的32位值。

●内存对齐可以帮助CPU更高效地访问内存中的数据
●结构对齐取决于大小保证和类型对齐保证
●地址对齐保证是:如果类型对齐t保证为n,那么运行时t类型的每个值的地址必须是n的倍数。
●如果结构体中的字段太满,可以尝试重新排列它们,使字段组织得更紧密,减少内存浪费
●将大小为零的字段作为结构体的最后一个字段会浪费内存
●在32位系统上对64位字的原子访问必须保证它们是8字节对齐的,当然如果不需要的话,使用锁(互斥锁)更清晰、更简单;

go-memoryAlign图解
doc-pdf


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

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

输出结果:

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

真实情况是怎样的?我们看一下调用本身,如下:

输出结果:

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

这里必须要提到“内存对齐”的概念,这样我们才能正确计算。接下来详细说一下它是什么

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

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

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

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

上图中,假设从Index1开始读取,有将会出现崩溃问题。因为内存访问限制没有调整。所以CPU会做一些额外的处理工作。如下:

从上面的过程可以得出,不做“内存调整”是有点“不方便”。因为会增加很多耗时的操作

假设内存对齐完成,从Index0读取4个字节,只需要读取一次,不需要额外的操作。这显然效率高很多,是一种用空间换时间的标准方法

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

另外,需要注意的是,不同的硬件平台所占用的大小和调整值可能会有所不同。因此,调试时本文中的值并不唯一,需要考虑机器的实际情况

输出结果:

在Go中,可以进行不确定的调用。.Alignof返回对应类型的对齐系数。通过观察输出结果,我们可以知道它们最初是2^n,最大值不会超过8。这是因为我的便携式(64位)编译器的默认调整因子是8,所以最大值不会超过8。超过这个数量

上一节提到结构体中的成员变量必须是字节对齐的。那么作为最终结果的结构体当然也必须是字节对齐的

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

各个成员变量之后是对齐的,根据规则2,整个结构本身也必须是字节对齐的,因为可能会发现它可能不是2^n,不是偶数。显然不遵循对齐规则

根据规则2,可以得出对齐值为8。此时偏移量为25,不是8的倍数。因此偏移量确定为32、调整结构

第1部分内存布局:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx

通过这部分的分析,我们可以知道上一篇《为什么“计算”是错误的?

这是因为实际的内存管理并不是按照“一根胡萝卜一个坑”的思想来进行的。这一段的读写通过以空间换时间(效率)的思想来完成,还需要考虑到不同平台的内存操作

上一节可以看出,根据类型而定。成员变量、结构体的内存都会进行对齐等操作。那么,假设字段顺序不同,会有什么变化吗?我们一起来试试:-)

输出结果:

通过结果我们可以惊讶地发现,仅仅“简单”的改变成员变量的字段顺序就改变了结构体涂层大小

那么我们就一起来分析第2部分,看看有什么是内饰和之前的不同,导致了这样的结果吗?

根据规则2,不需要额外调整

part2的内存布局:ecax|bbbb|dddd|dddd

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

仔细一看,part1中有很多padding。显然它占用了很大的空间,那么padding是什么样的呢?

通过本文的介绍,我们可以知道,不同类型需要字节对齐来保证内存访问限制

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


三、内存对齐详解1.什么是内存对齐?假设你声明了以下两个变量:
2.结构体的内存对齐规则
结构体及其成员占用的内存与其在结构体中声明的顺序有关。成员的内存对齐规则如下:
(1)每个成员根据自己的对齐字节数和PPB(指定对齐)进行分割。字节数,32位机器默认为4)将两者对齐到最小字节数以最小化长度。
(2)复杂类型(例如结构体)的默认对齐方式是最长成员的对齐方式,因此如果该成员是复杂类型,我可以最小化长度。
(3)结构体的对齐长度由最大对齐参数(PPB)决定,必须是)的整数倍。
(4)计算结构体内存大小时,列出各个成员的偏移地址及其长度=最后一个成员的偏移地址+最后一个成员编号的长度+最后一个Tuning成员参数(考虑PPB))。
3.案例描述:
—————————————————
最终输出如下没错:8
//>
4.注释
(1)字节对齐取决于编译器。
(2)请注意pragampack(n)指定的PPB大小。);
(3)结构体占用的字节数必须能被PPB整除。
(4)总结方程。该结构的大小等于最后一个成员的偏移量加上其大小加上末尾的填充字节数。即
sizeof(struct)=offsetof(lastitem)+sizeof(lastitem)+sizeof(trailingpadding)
———————————————