Swift结构体实例的内存布局的基础知识。

当您在内存中处理Swift类型时,需要考虑三个属性:大小、步幅和对齐(size, stride, and alignment)。

1. Size

让我们从两个简单的结构体开始:

struct Year {
  let year: Int
}

struct YearWithMonth {
  let year: Int
  let month: Int
}

我的直觉告诉我,YearWithMonth的实例比Year的实例更大——它在内存中占据了更多的空间。但我们是科技人员;我们如何用硬数字来验证直觉呢?

1.1. Memory Layout

我们可以使用MemoryLayout类型来检查关于类型在内存中的外观的一些属性。

要根据结构体的类型查找其大小,请使用size属性和泛型参数:

let size = MemoryLayout<Year>.size

如果你有一个该类型的实例,使用size(ofValue:)静态函数:

let instance = Year(year: 1984)
let size = MemoryLayout.size(ofValue: instance)

在这两种情况下,大小都报告为8字节。

结构体YearWithMonth的大小是16字节,这并不奇怪。

1.2. Back to Size

结构体的大小似乎很直观——计算每个属性的大小之和。对于像这样的结构:

struct Puppy {
  let age: Int
  let isTrained: Bool
}

大小应该匹配其属性的大小:

MemoryLayout<Int>.size + MemoryLayout<Bool>.size
// returns 9, from 8 + 1

MemoryLayout<Puppy>.size
// returns 9

似乎一切顺利! [旁白:真的吗?😈]

2. Stride

当您在一个缓冲区(例如数组)中处理多个实例时,类型的stride就变得非常重要。

如果我们有一个连续的小狗数组,每个小狗的大小为9个字节,那么在内存中会是什么样子呢?

avatar

事实证明,并非如此。❌

stride决定了两个元素之间的距离,它将大于或等于大小。

MemoryLayout<Puppy>.size
// returns 9

MemoryLayout<Puppy>.stride
// returns 16

所以布局实际上是这样的:

avatar

也就是说,如果您有一个指向第一个元素的字节指针,并且想要移动到第二个元素,那么stride就是指针向前移动所需要的字节距离。

为什么size和stride会不同? 这就把我们带到了内存布局的最终神奇数字。

3. Alignment

1字节(byte)=8位(bit)

在16位系统中,1字(word)=2字节(byte)=16位(bit)

在32位系统中,1字(word)=4字节(byte)=32位(bit)

在64位系统中,1字(word)=8字节(byte)=64位(bit)

想象一下,计算机每次获取8位,或者一个字节的内存。请求字节1或字节7分别需要相同的时间。

avatar

然后你升级到一台16位计算机,它可以访问16位字的数据。你仍然有旧的软件想要通过字节访问数据,但是想象一下这里可能的魔法: 如果软件需要字节0和字节1,计算机现在可以对word 0进行一次内存访问,并将16位结果分割。

在这种理想情况下,字节级内存访问速度是原来的两倍!🎉

现在假设一个流氓程序输入了一个16位的值:

avatar

然后你向计算机请求字节位置3的16位字。问题是值没有对齐。 要读它,电脑需要先读位置1的单词,把它切成两半,再读位置2的单词,把它切成两半,然后把两半粘贴在一起。这是两次单独的16位内存读取来访问一个16位值——速度比正常速度慢两倍!😭

在一些系统上,非对齐访问比慢更糟糕——它是不被完全允许的,并且会使程序崩溃。

3.1. Simple Swift Types

在Swift中,像Int和Double这样的简单类型的alignment值与它们的size相同。一个32位(4字节)整数的大小为4个字节,需要对齐到4个字节。

MemoryLayout<Int32>.size
// returns 4
MemoryLayout<Int32>.alignment
// returns 4
MemoryLayout<Int32>.stride
// returns 4

stride也是4,这意味着连续缓冲区中的值间隔为4字节。不需要填充。

3.2. Compound Types

现在回到我们的Puppy结构体,它有一个Int和一个Bool属性。再考虑一下在buffer中值彼此紧挨着的情况:

avatar

Bool值的位置很好,因为它们 alignment=1。但是第二个整数没有对齐。它是一个64位(8字节)值,对齐方式为8,并且它的字节位置不是8的倍数。❌

记住,这种类型的stride是16,这意味着缓冲区实际上看起来像这样:

avatar

我们保留了结构体中所有值的对齐要求:第二个整数位于字节16,是8的倍数。

这就是为什么结构体的stride可以大于它的size:增加足够的填充来满足对齐要求。

3.3. Calculating Alignment

在我们旅程的最后,Puppy结构类型的对齐是什么?

MemoryLayout<Puppy>.alignment
// returns 8

结构类型的对齐是其所有属性的最大对齐。在Int类型和Bool类型之间,Int类型的对齐值更大,为8,因此结构体使用它。

stride然后变成大小四舍五入到对齐的下一个倍数。在我们的例子中:

  • the size is 9
  • 9 is not a multiple of 8
  • the next multiple of 8 after 9 is 16
  • therefore, the stride is 16

4. One Last Complication

考虑我们最初的Puppy,并与AlternatePuppy进行对比:

struct Puppy {
  let age: Int
  let isTrained: Bool
} // Int, Bool

struct AlternatePuppy { 
  let isTrained: Bool
  let age: Int
} // Bool, Int

AlternatePuppy结构体仍然是8的对齐和16的步幅,但是:

MemoryLayout<AlternatePuppy>.size
// returns 16

发生了什么? ! 我们所做的只是改变了性质的顺序。为什么现在的大小不同了?还是应该是9,不是吗?Bool类型,后跟Int类型,如下所示:

avatar

也许您看到了这里的问题:8字节整数不再对齐!这在内存中是这样的:

avatar

结构本身必须对齐,结构内部的属性也必须保持对齐。 填充在元素之间移动,整个结构的大小就被扩展了。

在这种情况下,步幅仍然是16,所以从Puppy到AlternatePuppy的有效变化是填充的位置。这些结构体呢?

struct CertifiedPuppy1 {
  let age: Int
  let isTrained: Bool
  let isCertified: Bool
} // Int, Bool, Bool

struct CertifiedPuppy2 {
  let isTrained: Bool
  let age: Int
  let isCertified: Bool
} // Bool, Int, Bool

这两个结构的大小、步幅和对齐方式是什么?🤔(剧透)

5. The Closing Brace

最后,假设你有一个UnsafeRawPointer(在C语言中也叫**void ***),你知道它指向的是什么类型的东西。size、stride和alignment在哪里?

  • Size是从指针中读取所有数据所需的字节数。
  • Stride 为进入缓冲区中下一项所要前进的字节数。
  • Alignment是每个实例需要的“可被整除”的数字。如果你正在分配内存来复制数据,你需要指定正确的对齐方式(例如allocate(byteCount: 100, alignment: 4))。
avatar

对于我们大多数人来说,大多数时候,我们可能会处理高级集合,比如数组和集合,而不需要考虑底层的内存布局。

在其他情况下,您可以在平台上使用较低级别的api,或者与C代码进行互操作。如果你有一个Swift结构体数组并且需要你的C代码读它(反之亦然)你需要担心分配一个缓冲区使用正确的alignment,确保结构体内部填充对齐,并确保你有合适的stride,这样你就可以正确地解释数据。

正如我们所看到的,即使是计算大小也不像看起来那么简单——每个属性的大小和对齐之间有一些相互作用,决定了结构的总体大小。所以理解了这三种就意味着你正在成为内存大师。

更多?

🔗原文链接