当前位置:首页 > 百科 > 【Rust笔记】浅聊 Rust 程序内存布局

【Rust笔记】浅聊 Rust 程序内存布局

2024-04-27 22:10:47 [知识] 来源:白鱼登舟网

【Rust笔记】浅聊 Rust 程序内存布局

内存布局看似是笔记布局底层和距离应用程序开发比较遥远的概念集合,但其对前端应用的功能实现颇具现实意义。从业务模块至插件,无处不涉及到跨语言互操作。浅聊甚至,做个文本数据的程序字符集转换也得调用操作系统链接库,因为这意味着更小的发布文件。而与内存布局正是内存跨(计算机)语言数据结构的基础。

大约两个月前,在封装闭包(不是笔记布局函数指针)过程中,我重新梳理了内存布局知识点。然后,就有冲动写这么一篇长文。浅聊今恰逢国庆八天长假,汇总我之所知与大家分享。程序开始正文...

变量值在内存中的内存存储信息包含两个重要属性

  • 首字节地址

  • 存储宽度

而这两个值都不是在分配内存那一刻的“即兴选择”。而是笔记布局,遵循着一组规则:

  1. 与都必须是【对齐位数】的自然数倍。比如说,

  • 对齐位数等于字节的浅聊变量值可保存于任意内存地址上。

  • 对齐位数等于字节且有效数据长度等于字节的程序变量值

  • 存储宽度等于字节的变量值可接受任意正整数作为其对齐位数 — 惯例是字节。

  1. 仅能保存于偶数位的内存内存地址上。

  2. 存储宽度也得是笔记布局字节 — 从有效长度字节到存储宽度字节的扩容过程被称作“对齐”。

对齐位数必须是浅聊的自然数次幂。即,且是程序的自然数。

存储宽度是有效数据长度对齐填充位数的总和字节数 — 这一点可能有点儿反直觉。

,与的计量单位都是“字节”。

正是因为,与之间存在倍数关系,所以程序对内存空间的利用一定会出现冗余与浪费。这些被浪费掉的“边角料”则被称作【对齐填充】。对齐填充的计量单位也是字节。根据“边角料”出现的位置不同,对齐填充又分为

  • 端填充 — 填充位出现在有效数据侧的

  • 端填充 — 填充位出现在有效数据侧的

文字抽象,图直观。一图抵千词,请看图

0e4ab3bf9673123a343d160cc17e0812.png

延伸理解:借助于对齐位数,物理上一维的线性内存被重构为了逻辑上维的存储空间。不严谨地讲,一个数据类型 ➜ 对应一个对齐位数值 ➜ 按一个【单位一】将内存空间均分一遍 ➜ 形成一个仅存储该数据类型值(且只存在于算法与逻辑中)的维度空间。然后,在保存该数据类型的新值时,只要

  1. 选择进入正确的维度空间

  2. 跳过已被占用的【单位一】(这些【单位一】是在哪一个维度空间被占用、是被谁占用和怎么占用并不重要)

  3. 寻找连续出现且数量足够的【单位一】

就行了。

如果【对齐位数】与【存储宽度】在编译时已知,那么该类型就是【静态分派】。于是,

  • 类型的对齐位数可由std::mem::align_of::<T>()读取

  • 类型的存储宽度可由std::mem::size_of::<T>()读取

若【对齐位数】与【存储宽度】在运行时才可计算知晓,那么该类型就是【动态分派】。于是,

  • 的对齐位数可由std::mem::align_of_val::<T>(&T)读取

  • 的存储宽度可由std::mem::size_of_val::<T>(&T)读取

存储宽度的对齐计算

若变量值的有效数据长度正好是该变量类型【对齐位数】的自然数倍,那么该变量的【存储宽度】就是它的【有效数据长度】。即,。

否则,变量的【存储宽度】就是要大于等于【有效数据长度】,是【对齐位数】自然数倍的最小数值。

  • 这个计算过程的伪码描述是

  • 这个计算被称作“(自然数倍)对齐”。

基本数据类型

基本数据类型包括,,,,,,,,,,,,,,和。它们的内存布局在不同型号的设备上略有差异

  • 在非设备上,存储宽度 = 对齐位数(即,倍数)

  • 在设备上,因为设备允许的最大对齐位数不能超过字节,所以

    • 与的(即,)。

    • 与的(即,)。

    • 其它基本数据类型依旧(即,倍数)。

瘦指针

瘦指针的内存布局与类型是一致的。因此,在不同设备和不同架构上,其性能表现略有不同

  • 在非的

    • 位架构上,()

    • 位架构上,()

  • 在的

    •  — 设备最大对齐位数不能超过字节

    • 位架构上,()

    • 位设备上,

胖指针

胖指针的存储宽度是类型的倍,对齐位数却与相同。就依赖于设备/架构的性能表现而言,其与瘦指针行为一致:

  • 在非的

    • 位架构上,

    • 位架构上,

  • 在的

    •  — 设备最大对齐位数不能超过字节

    • 位架构上,

    • 位设备上,

数组,切片和

就是满足编码规范的增强版切片。

存储宽度是全部元素存储宽度之

对齐位数与单个元素的对齐位数一致。

单位类型

存储宽度 = 

对齐位数 = 

所有零宽度数据类型都是这样的内存布局配置。

来自【标准库】的零宽度数据类型包括但不限于:

  •  单位类型 — 模拟“空”。

  •  — 绕过“泛型类型形参必须被使用”的编译规则。进而,成就类型状态设计模式中的。

  •  — 禁止变量值在内存中“被挪来挪去”。进而,成就异步编程中的“自引用结构体”。

复合数据结构的内存布局描绘了该数据结构(紧内一层)字段的内存位置“摆放”关系(比如,间隙与次序等)。在层叠嵌套的数据结构中,内存布局都是就某一层数据结构而言的。它既承接不了来自外层父数据结构的内存布局,也决定不了更内层子数据结构的内存布局,更代表不了整个数据结构内存布局的总览。举个例子,

仅只代表最外层结构体的两个字段和是按内存布局规格“摆放”在内存中的。但,并不意味着整个数据结构都是内存布局的,更改变不了字段的类型是内存布局的事实。若你的代码意图是定义完全的结构体,那么【原始指针】才是该用的类型。

内存布局核心参数

自定义数据结构的内存布局包含如下五个属性

  • 定义:数据结构自身的对齐位数

  • 规则:

  1.  = 的次幂( 是的自然数)

  2. 不同于基本数据类型,自定义数据结构的算法随不同的数据结构而相异。

  • 定义:数据结构自身的宽度

  • 规则:必须是自然数倍。若有效数据长度不足,就添补空白【对齐填充位】凑足宽度。

  • 定义:每个字段的对齐位数

  • 规则: = 的次幂(是的自然数)

  • 定义:每个字段的宽度

  • 规则:必须是自然数倍。若有效数据长度不足,就添补空白【对齐填充位】凑足宽度。

  • 定义:每个字段首字节地址相对于上一层数据结构首字节地址的偏移字节数

  • 规则:

  1. 必须是自然数倍。若不足,就垫入空白【对齐填充位】和向后推移当前字段的起始位置。

  2. 前一个字段的 后一个字段的

自定义枚举类的内存布局一般与枚举类分辨因子的内存布局一致。更复杂的情况,请见下文章节。

预置内存布局方案

编译器内置了四款内存布局方案,分别是

  1. 默认内存布局 — 没有元属性注释

  2. 内存布局 

  3. 数字类型·内存布局 

  • 仅适用于枚举类。

  • 支持与内存布局混搭使用。比如,。

透明·内存布局 

  • 仅适用于单字段数据结构。

预置内存布局方案对比

相较于内存布局,内存布局面向内存空间利用率做了优化 — 省内存。具体的技术手段包括编译器

  • 重排了字段的存储顺序,以尽可能多地消减掉“边角料”(对齐填充)占用的字节位数。于是,在源程序中字段声明的词法次序经常不同于【运行时】它们在内存里的实际存储顺序

  • 允许多个零宽度字段共用一个内存地址。甚至,零宽度字段也被允许与普通(有数据)字段共享内存地址。

以中间格式为桥的内存布局虽然实现了跨语言数据结构,但它却更费内存。这主要出于两个方面原因:

  1. 内存布局未对字段存储顺序做优化处理,所以字段在源码中的词法顺序就是它们在内存条里的存储顺序。于是,若 @程序员 没有拿着算草纸和数着比特位“人肉地”优化每个数据结构定义,那么由对齐填充位冗余造成的内存浪费不可避免

  2. 内存布局不支持零宽度数据类型。零宽度数据类型是语言设计的重要创新。相比之下,

  • (参见规范的第节)字段结构体会导致标准程序出现,除非安装与开启的扩展。

  • 编译器会强制给无字段结构体安排一个字节宽度,除非该数据结构被显式地标记为。

以费内存为代价,内存布局赋予数据结构的另一个“超能力”就是:“仅通过变换【指针类型】就可将内存上的一段数据重新解读为另一个数据类型的值”。比如,被允许指向任意数据类型的变量值 例程。但在内存布局下,需要调用专门的标准库函数std::intrinsics::transmute()才能达到相同的目的。

除了上述鲜明的差别之外,与内存布局都允许【对齐位数】参数被微调,而不一定总是全部字段中的最大值。这包括但不限于:

  • 修饰符增加至指定值。例如,将内存布局中的【对齐位数】上调至字节

  • 修饰符减小至指定值。例如,将默认内存布局中的【对齐位数】下调至字节

结构体的内存布局

结构体算是最“中规中矩”的数据结构。无论是否对结构体的字段重新排序,只要将它们一个不落地铺到内存上就完成一多半功能了。所以,结构体存储宽度是全部字段之再(自然数倍)对齐于【结构体对齐位数】的结果。有点抽象上伪码

相较于内存布局优化算法的错综复杂,我好似只能讲清楚内存布局的始末:

首先,结构体自身的对齐位数就是全部字段对齐位数中的最大值

其次,声明一个(可修改的)游标变量以实时跟踪(参照于结构体首字节地址的)字节偏移量。游标变量的初始值为表示该游标与结构体的内存起始位置重合。

接着,沿着源码中字段的声明次序,逐一处理各个字段:

  1. 【对齐】若游标变量值不是当前字段对齐位数的自然数倍(即,未对齐),就计算大于等于是自然数倍的最小数值。并将计算结果更新入游标变量,以插入填充位对齐和向后推移字段在内存中的”摆放“位置。

  2. 【定位】当前游标的位置就是该字段的首字节偏移量

  3. 跳过当前字段宽度 — 递归算法求值数据结构的存储宽度。字段子数据结构的内存布局对上一层父数据结构是黑盒的。

  4. 继续处理下一个字段。

然后,在结构体内全部字段都被如上处理之后,

  1. 【对齐】若游标变量值不是结构体对齐位数的自然数倍(即,未对齐),就计算大于等于是自然数倍的最小数值。并将计算结果更新入游标变量,以增补填充位对齐和扩容有效数据长度至结构体存储宽度。

  2. 【定位】当前游标值就是整个结构体的宽度(含全部对齐填充位)

至此,结构体的内存布局结束。然后,就能够拿着这套“策划案”向操作系统申请内存空间去了。由此可见,每次【对齐】处理都会在有效数据周围“埋入”大量空白“边角料”(学名:对齐填充位)。但出于历史原因,为了完成与其它计算机语言的互操作,这些浪费还是必须的。下面附以完整的伪码辅助理解

联合体的内存布局

形象地讲,联合体是给内存中同一段字节序列准备了多套“数据视图”,而每套“数据视图”都尝试将该段字节序列解释为不同数据类型的值。所以,无论在联合体内声明了几个字段,都仅有一个字段值会被保存于物理存储之上。从原则上讲,联合体的内存布局一定与占用内存最多的字段一致,以确保任何字段值都能被容纳。从实践上讲,有一些细节处理需要斟酌:

  1. 联合体的对齐位数等于全部字段对齐位数中的最大值(同结构体)。

  2. 联合体的存储宽度是最长字段宽度值(自然数倍)对齐于联合体自身对齐位数的结果。有点抽象上伪码

举个例子,联合体内包含了与类型的两个字段,那么的内存布局就一定与的内存布局一致。再举个例子,

看答案之前,不防先心算一下,程序向标准输出打印的结果是多少。演算过程如下:

  1. 字段的

  • 存储宽度是字节。

  • 对齐位数也是字节,因为基本数据类型的【对齐位数】就是它的【存储宽度】。

字段的

  • 存储宽度是字节,因为数组的【存储宽度】就是全部元素存储宽度之

  • 对齐位数是字节,因为数组的【对齐位数】就是元素的【对齐位数】。

联合体的

  • 对齐位数就是字节,因为取最大值

  • 存储宽度是字节,因为得取最大值

再来一个更复杂点儿的例子,

同样,在看答案之前,不防先心算一下,程序向标准输出打印的结果是多少。演算过程如下:

  1. 字段的存储宽度与对齐位数都是字节。

  2. 字段的

  • 对齐位数是字节。

  • 存储宽度是字节。

联合体的

  • 对齐位数是字节 — 取最大值,没毛病。

  • 存储宽度是字节,因为不仅得取最大值字节,还得向自然数倍对齐。于是,才有了额外字节的【对齐填充】和扩容【联合体】有效长度字节至存储宽度字节。你猜对了吗?

不经意的巧合

思维敏锐的读者可以已经注意到:单字段【结构体】与单字段【联合体】的内存布局是相同的,因为数据结构自身的内存布局就是唯一字段的内存布局。不信的话,执行下面的例程试试

枚举类的内存布局

突破“枚举”字面含义的束缚,的创新使与传统计算机语言中的同类项都不同。枚举类

  • 既包括:风格的“装”枚举 — 仅标记状态,却不记录细节数据。

  • 也支持:风格的“装”枚举 — 标记状态的同时也记录细节数据。

在Rust References一书中,

  • 装”枚举被称为“无字段·枚举类 field-less enum”或“仅单位类型·枚举类 unit-only enum”。

  • 装”枚举被别名为“伴字段·枚举类”。

    • 在程序中,需要借助【标准库】的数据结构才能模拟出同类的功能来。欲了解更多技术细节,推荐读我的另一篇文章。

禁忌:内存布局的枚举类必须至少包含一个枚举值。否则,编译器就会报怨:。

“轻装”枚举类的内存布局

因为“轻装”枚举值的唯一有效数据就是“记录了哪个枚举项被选中的”分辨因子,所以枚举类的内存布局就是枚举类【整数类型】分辨因子的内存布局。即,

别庆幸!故事远没有看起来这么简单,因为【整数类】是一组数字类型的总称(馁馁的“集合名词”)。所以,它包含但不限于

RustC存储宽度u8 / i8unsigned char / char单字节u16 / i16unsigned short / short双字节u32 / i32unsigned int / int四字节u64 / i64unsigned long / long八字节usize / isize没有概念对等项,可能得元编程了等长于目标架构“瘦指针”宽度

维系两端和枚举类分辨因子都采用相同的整数类型才是最“坑”的,因为

  • 实例可存储任意类型的整数值(比如,,,和)— 部分原因或许是系语法灵活的定义形式:“块 + 具名常量”。所以,非常适合被做成“比特开关”。但在程序中,就不得不引入外部软件包bitflags了。

  • 内存布局枚举类分辨因子只能是类型 — 【存储宽度】是固定的字节。

    内存布局·枚举类·分辨因子的整数类型是编译时由决定的,但最宽支持到类型。

这就对端的程序设计提出了额外的限制条件:至少,由接口导出的枚举值得用类型定义。否则,端函数调用就会触发。门槛稍有上升。

扼要归纳:

  1. 端内存布局的枚举类对端枚举值的【整数类型】提出了“确定性假设”:枚举值的整数类型是且存储宽度等于字节。

  2. 端 @程序员 必须硬编码所有枚举值的数据类型,以满足该假设。

  3. 跨语言互操作才能成功“落地”,而不是发生。

来自端的迁就固然令人心情愉悦,但新应用程序难免要对接兼容遗留系统与旧链接库。此时,再给端提要求就不那么现实了 — 深度改动“屎山”代码风险巨大,甚至你可能都没有源码。【数字类型·内存布局】正是解决此棘手问题的技术方案:

  1. 以【元属性】注释枚举类定义

  2. 明确指示编译器采用给定【整数类型】的内存布局,组织【分辨因子】的数据存储,而不总是遵循内存布局。

从整数类型至内存布局元属性的映射关系包括但不限于

CRust 元属性unsigned char / char#[repr(u8)] / #[repr(i8)]unsigned short / short#[repr(u16)] / #[repr(i16)]unsigned int / int#[repr(u32)] / #[repr(i32)]unsigned long / long#[repr(u64)] / #[repr(i64)]

举个例子,

上面代码定义的是内存布局的“装”枚举类,因为它的每个枚举值不是“无字段”,就是“单位类型”。于是,的内存布局就是类型的。

再举个例子,

上面代码定义的是【数字类型·内存布局】的“装”枚举类。它的内存布局是类型的。

“重装”枚举类的内存布局

【“重装”枚举类】绝对是语言设计的一大创新,但同时也给跨语言互操作带来了严重挑战,因为在其它计算机语言中没有概念对等的核心语言元素“接得住它”。对此,在做内存布局时,编译器会将【“重装”枚举类】“降维”成一个双字段结构体:

  1. 第一个字段是:剥去了所有字段的【“轻装”枚举】,也称【分辨因子枚举类】。

  2. 第二个字段是:由枚举值内字段拼凑成的【结构体】组成的【联合体】。

前者记录选中项的“索引值” — 谁被选中;后者记忆选中项内的值:根据索引值,以对应的数据类型,读/写联合体实例的字段值。

文字描述着实有些晦涩与抽象。边看下图,边再体会。一图抵千词!(关键还是对数据类型的理解)

00dd48b817cfe6e0bc568d4f272c61a0.png

上图中有三个很细节的知识点容易被读者略过,所以在这里特意强调一下:

  1. 保存枚举值字段的结构体都派生了,派生了,因为

  • 数据结构要求它的每个字段都是可复制

  • 同时,又是的

降维后结构体内的字段名不重要,但字段排列次序很重要。因为在中,结构体字段的存储次序就是它们在源码中的声明次序,所以标准库中的数据结构总是,根据约定的字段次序,

  1. 将第一个字段解释为“选中项的索引号”,

  2. 将第二个字段解读为“选中项的数据值”。

内存布局的分辨因子枚举类的分辨因子依旧是类型值,所以端的枚举值仍旧被要求采用整数类型。

举个例子,

看答案之前,不防先心算一下,程序向标准输出打印的结果是多少。演算过程如下:

  1. 被“降维”成

  2. 就内存布局而言,的是全部字段中的最大值。

    1. 字段是字段元组结构体,且字段类型是基本数据类型。所以,

    2. 字段是单位类型。所以,和

    3. 于是,

    4. 字段是内存布局的“轻装”枚举类。所以,

    5. 字段是数据结构。所以,的也是全部字段中的最大值。

    6. 于是,

  3. 的是全部字段之和。

    1. 于是,

    2. 字段是内存布局的“轻装”枚举类。所以,

    3. 字段是数据结构。的是全部字段中的最大值。

    4. 于是,不精准地 (约等)

    5. 此刻并不是的自然数倍。所以,需要给增补“对齐填充位”和向自然数倍对齐

    6. 于是, (直等)

哎!看见没,内存布局还是比较费内存的,一少半都是空白“边角料”。

【“重装”枚举类】同样会遇到两端【枚举类分辨因子】与【枚举值】整数类型一致约定的难点。为了迁就端遗留系统和旧链接库对枚举值【整数类型】的选择,编译器依旧选择“降维”处理。但,这次不是将变形成,而是跳过封装和直接以为“话事人”。同时,将【分辨因子·枚举值】作为字段数据结构的个字段:

  • 元组枚举值,分辨因子就是数据结构第个元素

  • 结构体枚举值,分辨因子就数据结构第一个字段。注:字段名不重要,字段次序更重要

文字描述着实有些晦涩与抽象。边看下图,边对比上图,边体会。一图抵千词!

467ca9dc47af6b1bedb673a996a1f5ad.png

由上图可见,与【数字类型】的混合内存布局

  • 既保证了降级后与数据结构继续满足的存储格式要求。

  • 又确保了【端枚举类分辨因子】与【端枚举值】之间整数类型的一致性。

举个例子,假设目标架构是位系统,

看答案之前,不防先心算一下,程序向标准输出打印的结果是多少。演算过程如下:

  1. 被“降维”成

  2. 的是全部字段中的最大值。

    1. 第一个字段是类型的分辨因子枚举值。所以,

    2. 第二个字段是类型数字。所以,

    3. 于是,

    4. 字段是双字段元组结构体。所以,的是全部字段中的最大值。

    5. 字段是字段元组结构体且唯一字段就是分辨因子枚举值。所以,

    6. 于是,

  3. 的是全部字段中的最大值。

    1. 第一个字段是类型的分辨因子枚举值。所以,

    2. 第二个字段是类型数字。所以,

    3. 于是,不精准地(约等)

    4. 此刻不是的自然数倍。所以,需要对增补“对齐填充位”和向自然数倍对齐

    5. 于是, (直等)

    6. 字段是双字段元组结构体。所以,的是全部字段之

    7. 字段是字段元组结构体且唯一字段就是分辨因子枚举值。所以,

    8. 于是,

哎!看见没,C 内存布局还是比较费内存的,一少半的“边角料”。

新设计方案好智慧
  1. 优化掉了一层封装。即,从缩编至

  2. 将被优化掉的的职能(— 记录选中项的“索引值”)合并入了字段的数据结构中。于是,联合体的每个字段

  • 既要,保存枚举值的字段数据 — 旧职能

  • 还要,记录枚举值的“索引号” — 新职能

但有趣的是,比较上一版数据存储设计方案,内存布局却没有发生变化。逻辑描述精简了但物理实质未变,这太智慧了!因此,由标准库提供的数据结构依旧“接得住”端【“重装”枚举值】

仅【数字类型·内存布局】的“重装”枚举类

若不以【数字类型】的混合内存布局来组织枚举类的数据存储,而仅保留【数字类型】内存布局,那么上例中被降维后的【联合体】与【结构体】就都会缺省采用内存布局。参见下图:

423bc8c42387cf9ef017013f69bf0c99.png

补充于最后,思维活跃的读者这次千万别想太多了。没有的内存布局组合,因为【透明·内存布局】向来都是“孤来孤往”的。

数字类型·内存布局

仅【枚举类】支持【数字类型·内存布局】。而且,将无枚举值的枚举类注释为【数字类型·内存布局】会导致编译失败。举个例子

会导致编译失败。

透明·内存布局

“透明”不是指“没有”,而是意味着:在层叠嵌套数据结构中,外层数据结构的【对齐位数】与【存储宽度】等于(紧)内层数据结构的【对齐位数】和【存储宽度】。因此,它仅适用于

  • 单字段的结构体 — 结构体的【对齐位数】与【存储宽度】等于唯一字段的【对齐位数】和【存储宽度】。

  • 单枚举值且单字段的“重装”枚举类 — 枚举类的【对齐位数】与【存储宽度】等于唯一枚举值内唯一字段的【对齐位数】和【存储宽度】。

  • 单枚举值的“轻装”枚举类 — 枚举类的【对齐位数】与【存储宽度】等于单位类型的【对齐位数】和【存储宽度】。

原则上,数据结构中的唯一字段必须是非零宽度的。但是,若【透明·内存布局】数据结构涉及到了

  • 类型状态设计模式

  • 异步多线程

,那么内存布局的灵活性也允许:结构体和“重装”枚举值额外包含任意数量的零宽度字段。比如,

  •  为类型状态设计模式,提供支持。

  •  为自引用数据结构,提供支持。

举个例子,

看答案之前,不防先心算一下,程序向标准输出打印的结果是多少。演算过程如下:

  1. 因为字段是零宽度数据类型,所以它的

    和不参与内存布局计算。

  • 首字节地址与字段重叠。

因为【透明·内存布局】,所以 外层枚举类的

  • 【对齐位数】

  • 【存储宽度】

不同于【数字类型·内存布局】,【透明·内存布局】不被允许与其它内存布局混合使用。比如,

  • 是合法的

  • 和就会导致语编译失败

其它类型的内存布局

  • 与由胖指针/引用的变量值的【内存布局】相同。

  • 闭包没有固定的【内存布局】。

微调内存布局

只有与内存布局具备微调能力,且只能修改【对齐位数】参数值。另外,不同数据结构可做的微调操作也略有不同:

  • ,,数据结构可调对齐位数

  • 仅,被允许调对齐位数

数据结构【对齐位数】值的增加与减少需要使用不同的元属性修饰符

  •  增加对齐位数新值。将小于等于数据结构原本对齐位数的值输入修饰符是无效的。

  •  减少对齐位数新值。将大于等于数据结构原本对齐位数的值输入修饰符也是无效的。

与修饰符的实参是【目标】字节数,而不是【增量】字节数。所以,指示编译器增加对齐数至字节,而不是增加字节。另外,新对齐位数必须是的自然数次幂。

禁忌

  • 同一个数据类型不被允许既增加又减少对齐位数。即,与修饰符不能共同注释一个数据类型定义。

  • 减小对齐位数的外层数据结构禁止包含增加对齐位数的子数据结构。即,数据结构不允许嵌套包含子数据结构。

枚举类内存布局的微调

首先,枚举类不允许调对齐位数。

其次,上调枚举类的对齐位数也会触发“内存布局重构”的负作用。编译器会效仿Newtypes 设计模式重构枚举类为嵌套包含了的元组结构体。一图抵千词,请参阅下图。

101000f8442c00fd01169ed94d872644.png

由上图可见,在内存布局重构之后,内存布局继续保留在枚举类上,而修饰符仅对外层的结构体有效。所以,从底层实现来讲,枚举类是不支持内存布局微调的,仅能借助外层的数据结构间接限定。

以上面的数据结构为例,

看答案之前,不防先心算一下,程序向标准输出打印的结果是多少。演算过程如下:

  • 因为内存布局,所以枚举类的分辨因子是类型和枚举类的存储宽度。

  • 但,将内存空间占用强制地从提升到。

这次分享的内容比较多,感谢您耐心地读到文章结束。文章中问答式例程的输出结果,您猜对了几个呀?

内存布局是一个非常宏大技术主题,这篇文章仅是抛砖引玉,讲的粒度比较粗,涉及的具体数据结构也都很基础。更多和内存布局的实践经验沉淀与知识点汇总,我将在相关技术线的后续文章中陆续分享。

路过神仙哥哥与仙女妹妹们,多给文章发评论与点赞呀!

(责任编辑:时尚)

推荐文章
热点阅读