共计 2564 个字符,预计需要花费 7 分钟才能阅读完成。
一、实践
1、什么是内存对齐
古代计算机中内存空间都是依照 byte 划分的,在计算机中拜访一个变量须要拜访它的内存地址,从实践上看,仿佛对任何类型的变量的拜访都能够从任何地址开始。
但在理论状况中,通常在特定的内存地址能力拜访特定类型变量,这就须要对数据在内存中寄存的地位有限度。各种类型不是依照程序排放,它们须要依据肯定的规定在空间上排列,这就是对齐。
2、为什么须要内存对齐
(1)移植起因:
不是所有的硬件平台都能拜访任意地址上的任意数据的,各个硬件平台对存储空间的解决上有很大的不同,局部平台对某些特定类型的数据只能从某些特定地址开始存取。
比方,市面上有些架构的 CPU 在拜访一个没有进行对齐的变量时会产生谬误,这时 CPU 会进入异样解决状态并且告诉程序不能继续执行。
举个例子,在 ARM 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异样。所以,如果不进行内存对齐,代码就不具备移植性,而且难以发展在很多平台上的开发工作。
(2)性能起因:
只管内存是以字节为单位,然而大部分处理器并不是按字节块来存取内存的。它个别会以双字节、4 字节、8 字节、16 字节甚至 32 字节为单位来存取内存,咱们将上述这些存取单位称为内存存取粒度。
如果变量的地址没有对齐,可能须要屡次拜访能力残缺读取到变量内容,而对齐后可能就只须要一次内存拜访。因而,内存对齐能够缩小 CPU 拜访内存的次数,进步 CPU 拜访内存的吞吐量。
举个例子,思考 4 字节存取粒度的处理器拜访 int 类型变量,该处理器只能从地址为 4 的倍数的内存地址开始读取数据。如果未通过内存对齐,获取该 int 类型的数据须要进行两次内存拜访,最初再进行数据整顿失去残缺数据:
如果通过内存对齐,一次内存拜访就能失去残缺数据,缩小了一次内存拜访:
CPU 读取内存是高耗时的指令,内存对齐是在内存的使用量和 CPU 计算间的居中的优化策略。这种策略是由编译器和 CPU 独特决定,并且程序员能够设置对齐的长度。
通过上述介绍的内存对齐的必要性,咱们能够晓得,如果不了解内存对齐,在编程时就可能产生上面的问题:
(1) 程序运行速度变慢;
(2) 应用程序产生死锁;
(3) 操作系统解体;
(4) 程序会毫无征兆的出错,产生谬误的后果。
然而,咱们在写程序时个别无需思考对齐,通常是依赖编译器来为咱们抉择适宜的对齐策略。
3、内存对齐规定
在学习内存对齐规定前,咱们先一起理解下四个重要的基本概念:
- 指定对齐值:
pragma pack (n) 时指定的对齐值 n; - 根本数据类型的本身对齐值:
根本数据类型本身占用的存储空间大小,如 char 类型为 1,short 类型为 2,int 类型为 4,double 类型为 8 等; - 构造体或类类型的本身对齐值:
构造体或类的成员中本身对齐值最大的值,如 struct a 中有 char、int 和 double 共 3 个类型的数据成员,那么 struct a 的本身对齐值是 8 字节; - 数据成员、构造体和类的无效对齐值:
本身对齐值或指定对齐值中的较小值。
理解上述概念后,咱们一起来理解具体的内存对齐规定。
内存地址对齐蕴含了两种互相独立又互相关联的局部:根本数据对齐和构造体数据对齐。
根本数据对齐比较简单,其本身对齐值就等于本身占用的存储空间大小,能够通过 alignof 获取一个类型的对齐值。
构造体数据对齐须要保障构造体的数据成员对齐以及构造体的整体对齐:
(1)数据成员对齐规定:
第一个数据成员放在 offset 为 0 的中央,也就是构造体变量自身的起始地址,当前每个数据成员的偏移为其无效对齐值的整数倍;
(2)构造体整体对齐规定:
在数据成员实现各自对齐后,构造体自身也要进行对齐,对齐会将构造体的大小减少为该构造体无效对齐值的整数倍,如有须要编译器会在最末成员后加上填充字节。
二、利用
1、前提介绍
在 KaiwuDB 时序引擎中,一条时序数据由不同的列数据组成,其中每列都对应一种数据类型。在存储的代码实现中,每条时序数据都寄存在一块间断的内存空间里,不同列的数据依照列的程序间断紧邻的排放,如下图所示:
值得一提的是,上图示例中的 char 类型,并非是一个字符,而是代表一个不定长的字符数组,也就是说这条数据的第二列,寄存的是一个长度为 3 的 char 数组。另外,TIMESTAMP 的类型是 uint64_t,长度为 8 字节;Bitmap 的类型也是 char 数组,长度不定。
显然,这种寄存形式并不满足内存对齐的要求,会对咱们的程序产生两种可能的影响:
(1)在不同硬件平台上的程序 crash;
(2)升高存取效率。
因为咱们的程序须要在 ARM 平台上运行,而且会有高密集地进行内存拜访,所以时序数据寄存满足内存对齐要求是十分必要的。
2、应用内存对齐生成存储格局
存储记录的数据类型能够分类为两种:
(1)定长:
TIMESTAMP、SMALLINT、INT、BIGINT、FLOAT、DOUBLE、BOOL、BINARY,长度包含 8、4、2、1 字节,都属于根本数据类型。前文介绍过,根本数据类型的对齐系数等于本身的类型长度。
(2)不定长:
char、Bitmap,这两种类型都是不定长的 char 数组,根本组成元素都是 char 类型,因而它们的对齐系数都为 1。
由上可知,目前存储中寄存的都是一些根本数据类型记录,不存在构造体或类类型。在这个条件下,一条时序数据的格局满足内存对齐的要求就相对而言比较简单了,存储格局生成计划如下:
对于定长的数据类型对应的列,按类型长度降序排列,先寄存 8 字节的列、再寄存 4 字节的列、再寄存 2 字节和 1 字节的列,这样就能够满足内存对齐的要求。
寄存完定长的列后,char 和 Bitmap 都是 char 数组,对齐系数都为 1,所以能够间接寄存在定长的列之后,先寄存 char 类型,最初寄存 Bitmap。
通过上述内存对齐的存储格局生成计划生成的一条时序数据的格局如下:
3、测试
通过编写一个简略的测试,验证一下内存对齐对存取效率的影响。
测试场景:
内存对齐系数为 8 字节,申请 10K 内存空间,在内存对齐和非内存对齐的状况下,别离写入数据 1G 次并且读取数据 1G 次。写入的数据类型均为 uint64_t,长度 8 字节,内存写满后循环应用。别离统计其耗时并进行比照,测试代码和测试后果如下:
测试结果显示,在只进行读写操作的状况下,进行 1G 次读写操作,非内存对齐要比内存对齐的耗时多 24.3% 左右。