Swift 中的栈和堆误解

Stack and heap misunderstanding in Swift

我一直都知道引用类型变量存放在堆中,而值类型变量存放在栈中。最近发现这张图说int、double、string等是值类型,而函数和闭包是引用类型:

现在我真的很困惑。那么,当整数、双精度数、字符串等定义在 class(也称为引用类型)中时,它们保存在哪里?在同一类型中,当定义在结构内部时,函数在哪里保存闭包,也就是值类型?

I've always known that reference type variables are stored in the heap while value type variables are stored in the stack.

这在 Swift 中只是部分正确。通常,Swift 不保证对象和值的存储位置,除了:

  1. 引用类型在内存中有一个稳定的位置,因此对同一对象的所有引用都指向完全相同的位置,并且
  2. 值类型保证在内存中有一个稳定的位置,并且可以在编译器认为合适的情况下任意复制

技术上意味着如果编译器知道一个对象是在同一个堆栈帧中创建和销毁的,并且没有对它的转义引用,则对象类型可以存储在堆栈中,但实际上,您基本上可以假设所有对象都分配在堆上。

对于值类型,情况稍微复杂一些:

  • 除非值需要 location-based 引用(例如,使用 & 引用结构),否则结构可能完全位于 registers:对小型结构进行操作可能会将其成员放入 CPU 寄存器中,因此它甚至永远不会存在于内存中。 (对于像 Ints 和 Doubles 这样的小的,可能 short-lived 的值类型尤其如此,它们保证适合寄存器)
  • 大值类型 do 实际上得到 heap-allocated:虽然这是 Swift 的实现细节,理论上将来可能会改变,但结构是大于 3 个机器字(例如,在 32 位机器上大于 12 字节,或在 64 位机器上大于 24 字节)几乎可以保证分配并存储在堆上。这与值类型的 value-ness 不冲突:它仍然可以按照编译器的意愿任意复制,并且编译器在避免不必要的分配方面做得非常好

So where are ints, doubles, strings, etc. are kept when they are defined inside a class, aka reference type?

这是一个很好的问题,它是什么是值类型的核心。考虑值类型存储的一种方法是 inline,无论它需要在哪里。想象一个

struct Point {
    var x: Double
    var y: Double
}

结构,在内存中布局。暂时忽略 Point 本身是一个结构的事实,xy 相对于 Point 存储在哪里?那么,inline 无论 Point 去哪里:

┌───────────┐
│   Point   │
├─────┬─────┤
│  x  │  y  │
└─────┴─────┘

当您需要存储一个 Point 时,编译器会确保您有足够的 space 来存储 xy,通常一个紧接着另一个.如果一个Point入栈,那么xy入栈,一个接一个;如果 Point 存储在堆上,则 xy 作为 part 存在于 heapPoint。无论 Swift 在哪里放置一个 Point,它总是确保你有足够的 space,并且当你分配给 xy 时,它们被写入那个 space。它在哪里并不重要。

Point 另一个 对象的一部分时?例如

class Location {
    var name: String
    var point: Point
}

然后 Point 也被布置成 inline 存储在任何地方,并且 its 值也被布置成内联:

┌──────────────────────┐
│       Location       │
├──────────┬───────────┤
│          │   Point   │
│   name   ├─────┬─────┤
│          │  x  │  y  │
└──────────┴─────┴─────┘

在这种情况下,当您创建一个 Location 对象时,编译器会确保有足够的 space 来存储一个 String 和两个 Double,并放置他们一个接一个地出来。同样,它在哪里并不重要,但在这种情况下,它是堆上的 all(因为 Location 是引用类型,恰好 包含 个值)。


至于反过来,对象存储必须组件:

  1. 您用来访问对象的变量,以及
  2. 对象的实际存储空间

假设我们将 Point 从结构更改为 class。以前Location直接存储Point的内容,现在只存储一个reference到它们在内存中的实际存储:

┌──────────────────────┐      ┌───────────┐
│       Location       │ ┌───▶│   Point   │
├──────────┬───────────┤ │    ├─────┬─────┤
│   name   │   point ──┼─┘    │  x  │  y  │
└──────────┴───────────┘      └─────┴─────┘

以前,当Swift布局space创建一个Location时,它存储了一个String和两个Double;现在,它将一个 String 和一个 指针 存储到 Point。与 C 或 C++ 等语言不同,您实际上不需要意识到 Location.point 现在是一个指针这一事实,它实际上并没有改变您访问对象的方式;但在幕后,Location 的大小和“形状”发生了变化。

这同样适用于存储所有其他引用类型,包括闭包。持有闭包的变量在很大程度上只是指向闭包某些元数据的指针,以及一种执行闭包代码的方法(尽管具体细节超出了本答案的范围):

┌───────────────────────────────┐     ┌───────────┐
│           MyStruct            │     │  closure  │
├─────────┬─────────┬───────────┤ ┌──▶│  storage  │
│  prop1  │  prop2  │  closure ─┼─┘   │  + code   │
└─────────┴─────────┴───────────┘     └───────────┘