10 KiB
Swift 枚举值内存布局
enum 使用很简单,那大家有没有思考过系统针对枚举的实现是怎么样的?接下去会针对不同情况的枚举,结合汇编来窥探下系统实现原理。
基础枚举
enum Season {
case spring
case summer
case antumn
case winter
}
var season: Season = Season.spring
print(Mems.ptr(ofVal: &season))
season = Season.summer
season = Season.antumn
print("over")
-
var season: Season = Season.spring基础枚举,默认值是0。
-
season = Season.summer,此时可以看到第一个字节的位置是1.
-
season = Season.antumn,此时可以看到第一个字节的位置是2
结论:查看内存信息,可以看到基础枚举,只占1个字节大小空间,且值为默认值。
只有原始值
enum Season:Int {
case spring = 1
case summer = 2
case antumn = 3
case winter = 4
}
//print(MemoryLayout<Season>.size)
//print(MemoryLayout<Season>.stride)
//print(MemoryLayout<Season>.alignment)
var season: Season = Season.spring
print(Mems.ptr(ofVal: &season))
season = .summer
season = .winter
print("over")
-
var season: Season = Season.spring基础枚举,变量默认值,可以看到第一个字节的位置是0
-
season = .winter基础枚举,当赋值为 winter 的时候,可以看到第一个字节的位置是3
结论:带有原始值的枚举,同样只占用1个字节,该字节的值为枚举的位置(比如 case1 case2)
带有关联值的枚举
enum Season {
case spring(Int, Int, Int)
case summer(Int, Int)
case antumn(Int)
case winter(Bool)
case unknown
}
print(MemoryLayout<Season>.size)
print(MemoryLayout<Season>.stride)
print(MemoryLayout<Season>.alignment)
var season: Season = Season.spring(1, 2, 3)
print(Mems.ptr(ofVal: &season))
season = Season.summer(4, 5)
season = Season.antumn(6)
season = Season.winter(true)
season = Season.unknown
print("over")
-
var season: Season = Season.spring(1, 2, 3)带有关联值的枚举,.spring有3个 Int,单个 Int 占8个字节空间,所以红色框代表 spring 的1,蓝色框代表 spring 的2,绿色框代表 spring 的3,黄色框代表枚举的第1个 case,剩余7个字节,为空。
其内存信息如下(8字节为1组,对应上图)
01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00这段内存信息怎么看?我划分了下
关联值: 01 00 00 00 00 00 00 00 关联值: 02 00 00 00 00 00 00 00 关联值: 03 00 00 00 00 00 00 00 位置值: 00 内存对齐占用:00 00 00 00 00 00 00下面的几组一样
-
season = Season.summer(4, 5)带有关联值的枚举,.summer有2个 Int,单个 Int 占8个字节空间,所以红色框代表 summer 的4,蓝色框代表 summer 的5,绿色框为空,黄色框代表枚举的第2个 case,剩余7个字节,为空。
其内存信息如下(8字节为1组,对应上图)
04 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 -
season = Season.antumn(6) 带有关联值的枚举,.antumn有1个 Int,单个 Int 占8个字节空间,所以红色框代表 antumn 的6,蓝色框为空,绿色框为空,黄色框代表枚举的第3个 case,剩余7个字节,为空。
其内存信息如下(8字节为1组,对应上图)
06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 -
season = Season.winter(true)带有关联值的枚举,.winter` 有1个 Bool,单个 Int 占1个字节空间,所以红色框代表 winter 的 true,蓝色框为空,绿色框为空,黄色框代表枚举的第4个 case,剩余7个字节,为空。
其内存信息如下(8字节为1组,对应上图)
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 -
season = Season.unknown带有关联值的枚举,unknown没有关联值,所以红色框为空,蓝色框为空,绿色框为空,黄色框代表枚举的第5个 case,剩余7个字节,为空。
其内存信息如下(8字节为1组,对应上图)
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 -
MemoryLayout<Season>.size:3个 Int 最大为3*8,1个字节用来表达位置信息,3*8 + 1 = 25 -
MemoryLayout<Season>.stride:获取系统分配给数据类型的内存大小,也就是实际内存大小(对齐后的) -
MemoryLayout<Season>.alignment内存对齐系数,以8 Byte 为单位,对象分配的内存必须是该值的整数倍
只有一个 case 的枚举
enum SimpleEnum {
case one
}
var caseOne = SimpleEnum.one
print(MemoryLayout<SimpleEnum>.size) // 0
print(MemoryLayout<SimpleEnum>.stride) // 1
print(MemoryLayout<SimpleEnum>.alignment) // 1
为什么 size 为0?看上去是一个变量,但根本不占内存。因为枚举里面就一个 case,所以里面根本不需要存储值来区分是哪个 case。
enum SimpleEnum {
case one
case two
}
var caseOne = SimpleEnum.one
print(MemoryLayout<SimpleEnum>.size) // 1
print(MemoryLayout<SimpleEnum>.stride) // 1
print(MemoryLayout<SimpleEnum>.alignment) // 1
现在好理解,2个 case 需要存储1个 Byte 的值来区分是哪个 case,1 Byte 可以代表最多256个 case
只有1个 case 且带关联值的枚举
enum SimpleEnum {
case one(Int)
}
var caseOne = SimpleEnum.one(4)
print(MemoryLayout<SimpleEnum>.size) // 8
print(MemoryLayout<SimpleEnum>.stride) // 8
print(MemoryLayout<SimpleEnum>.alignment) // 8
带有关联值且只有1个 case 的枚举,因为有1个 Int 的关联值,但只有1个 case,所以只需要8 Byte 存储关联值即可。
请看下面的对照实验
enum SimpleEnum {
case one(Int)
case two
}
var caseOne = SimpleEnum.one(4)
print(MemoryLayout<SimpleEnum>.size) // 9
print(MemoryLayout<SimpleEnum>.stride) // 16
print(MemoryLayout<SimpleEnum>.alignment) // 8
2个 case,其中一个 case 有关联值 Int,所以需要8 Byte 存 Int 值,1 Byte 区分是哪个 case,实际需要占用 8 + 1 = 9 Byte,内存对齐单位是8,9向上为16.
用汇编验证下内存
enum Season {
case spring(Int, Int, Int)
case summer(Int, Int)
case antumn(Int)
case winter(Bool)
case unknown
}
var season: Season = Season.spring(1, 2, 3)
print(Mems.ptr(ofVal: &season))
season = Season.summer(4, 5)
season = Season.antumn(6)
season = Season.winter(true)
season = Season.unknown
print("over")
断点停到 var season: Season = Season.spring(1, 2, 3) 位置
将断点处的汇编单独摘出来研究
0x10000334b <+11>: movq $0x1, 0x8eaa(%rip) ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
0x100003356 <+22>: movq $0x2, 0x8ea7(%rip) ; SwiftDemo.season : SwiftDemo.Season + 4
0x100003361 <+33>: movq $0x3, 0x8ea4(%rip) ; SwiftDemo.season : SwiftDemo.Season + 12
0x10000336c <+44>: movb $0x0, 0x8ea5(%rip) ; SwiftDemo.season : SwiftDemo.Season + 23
0x100003373 <+51>: movl $0x1, %edi
rip 存储的说指令的地址。CPU 要执行的下一条指令地址就存储在 rip 中。所以在执行第一行的时候,rip 寄存器的值。
所以第一句汇编代码的意思是:rip 为 0x100003356,再加上 0x8eaa,得到一个地址值(用 Mac 自带的计算器可以算出)0X10000C200,然后 movq 是将十六进制的1赋值给 0X10000C200 这个地址。
第二句汇编代码类似,此时 rip 为 0x100003361,再加上 0x8ea7,得到一个地址值 0X10000C208,然后 movq 将十六进制的2赋值给 0X10000C208 这个地址。
第三句汇编代码类似,此时 rip 为 0x10000336c,再加上 0x8ea4,得到一个地址值 0X10000C210,然后 movq 将十六进制的3赋值给 0X10000C210 这个地址。
第四句汇编代码类似,此时 rip 为 0x100003373,再加上 0x8ea5,得到一个地址值 0X10000C218,然后 movq 将十六进制的0赋值给 0X10000C218 这个地址。
此时断点走到下一行,拿到 season 的内存地址 0X10000C200 ,查看内存发现和上面理论分析一直
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
结论:如果枚举存在关联值,内存大小为:
- 1个字节用来存储成员值
- n个字节用来存储关联值(n取占用内存最大的关联值),任何一个 case 的关联值都共用这 n 个字节
- 且存在内存对齐,所以占用大小为 n 和 1 的最大值,再结合内存对齐。
- 如果枚举的定义非常简单,系统会用1个字节来存放值,最大范围是256个 case。
- 枚举定义如果有原始值,也不会影响内存布局。