在 Swift 底层,Any 类型的变量并不是直接存储数据的,而是通过一个被称为 Existential Container(存在容器) 的结构来管理的。
由于 Any 可以代表任何类型(从小型的 Int 到大型的 Struct,再到指针形式的 Class),它们的内存大小各不相同。为了让编译器能以统一的方式处理这些大小不一的类型,Swift 引入了 Existential Container。
一个标准的 Existential Container 占用 5个内存字(Words) ,在 64 位架构下共 40 字节。其内部结构如下:
1. 内存结构布局
Existential Container 由三个部分组成:
- Value Buffer(值缓冲区) :占 3 个字(24 字节)。用于直接存储值或存储指向堆内存的指针。
- VWT (Value Witness Table,值见证表) :占 1 个字(8 字节)。负责管理值的生命周期(拷贝、销毁、分配)。
- PWT (Protocol Witness Table,协议见证表) :占 1 个字(8 字节)。如果
Any实际上是某个协议类型,这里存储该协议的方法映射;对于纯Any,这部分逻辑稍有不同,但结构是相似的。
2. 存储值类型 (Struct / Enum)
当你在 Any 中存储值类型时,Swift 会根据其大小采取不同的策略:
A. 小型值类型(≤ 24 字节)
如果值的大小(如 Int, Double, 或包含少量属性的 Struct)小于或等于 24 字节:
- 直接存储:值会直接填充在 Value Buffer 的 3 个字空间内。
- 无需堆分配:这种情况下性能很高,没有额外的内存分配开销。
B. 大型值类型(> 24 字节)
如果结构体非常大(比如一个包含 10 个 Double 的 Struct):
- 堆分配:Swift 会在堆(Heap)上开辟一块空间来存放这个结构体。
- 存储指针:Value Buffer 的第一个字(First Word)会存储指向该堆内存地址的指针。
- 性能损耗:由于涉及堆分配和引用计数管理(尽管是值类型,但容器需要管理这块内存的生命周期),性能会比直接存储低。
3. 存储引用类型 (Class)
当你在 Any 中存储类实例时,情况相对简单:
- 存储指针:因为 Class 本身就是引用类型,其本质就是一个指针。
- Value Buffer:Value Buffer 的第一个字会存储指向该类实例在堆中地址的指针。
- 引用计数:存入和取出时,VWT 会负责调用
swift_retain和swift_release来维护该对象的引用计数。
4. VWT 的核心作用
无论存储的是什么,Any 容器都必须知道如何处理它。VWT (Value Witness Table) 是关键:
- 当你把
Any赋值给另一个Any时,容器会查看 VWT,如果是小型值则直接按位拷贝 Buffer,如果是大型值或引用类型,则通过 VWT 进行深拷贝或增加引用计数。 - 当
Any变量作用域结束时,VWT 负责释放内存。
总结对比
| 存储类型 | 存储位置 | Value Buffer 内容 | 性能 |
|---|---|---|---|
| 小 Struct (≤24B) | 栈(Container 内) | 实际数据内容 | 高(无堆开销) |
| 大 Struct (>24B) | 堆(Heap) | 指向堆的指针 | 中(有堆开销) |
| Class (引用类型) | 堆(Heap) | 指向堆对象的指针 | 中(有 ARC 开销) |
为什么这很重要?
理解这一点能帮你优化 Swift 性能:频繁在 Any 或协议类型中传递大型结构体会导致意外的堆分配开销。 如果对性能有极致要求,建议使用泛型(Generics),因为泛型在编译期会进行“特化(Specialization)”,从而避免使用这种昂贵的 Existential Container。
英文版
7-12. [Advanced] How does the Existential Container store Values and References when using the Any type?
At the lower levels of Swift, a variable of type Any does not store data directly. Instead, it is managed through a structure known as the Existential Container.
Because Any can represent types of varying sizes—from a small Int to a large Struct or a Class pointer—the compiler needs a uniform way to handle them. The Existential Container provides this consistent interface.
A standard Existential Container occupies 5 memory words (40 bytes on a 64-bit architecture). Its internal structure is as follows:
1. Memory Layout Breakdown
The Existential Container consists of three distinct parts:
- Value Buffer: Occupies 3 words (24 bytes). This area either stores the value directly or holds a pointer to heap-allocated memory.
- VWT (Value Witness Table) : Occupies 1 word (8 bytes). This table is responsible for managing the lifecycle of the value (copying, destroying, and allocating).
- PWT (Protocol Witness Table) : Occupies 1 word (8 bytes). If the
Anyinstance is being treated as a specific protocol type, this table stores the mapping of the protocol's methods. For a pureAnytype, the structure remains similar though the logic varies slightly.
2. Storing Value Types (Struct / Enum)
When you store a value type in an Any container, Swift adopts different strategies based on the size of the type:
A. Small Value Types (≤ 24 bytes)
If the size of the value (such as an Int, Double, or a small Struct) is 24 bytes or less:
- Inline Storage: The data is stored directly within the 3-word Value Buffer.
- Performance: This is highly efficient as it requires no heap allocation.
B. Large Value Types (> 24 bytes)
If the struct is large (e.g., a Struct containing ten Double properties):
- Heap Allocation: Swift allocates a block of memory on the Heap to store the struct.
- Pointer Storage: The first word of the Value Buffer stores a pointer to that heap address.
- Performance: There is a performance hit due to heap allocation and the overhead of managing that memory's lifecycle.
3. Storing Reference Types (Class)
When storing a class instance in Any, the logic is more straightforward:
- Pointer Storage: Since a Class is a reference type, it is inherently a pointer.
- Value Buffer: The first word of the Value Buffer stores the pointer to the class instance on the heap.
- Reference Counting: The VWT handles calls to
swift_retainandswift_releaseto maintain the object's reference count as it moves in and out of the container.
4. The Critical Role of the VWT
Regardless of what is being stored, the Any container must know how to manipulate it. The Value Witness Table (VWT) provides the "instructions":
- When an
Anyis assigned to anotherAny, the container consults the VWT. If it's a small value, it performs a bitwise copy of the buffer; if it's a large value or reference type, the VWT handles the deep copy or increments the reference count. - When the
Anyvariable goes out of scope, the VWT is responsible for deallocating or releasing the associated memory.
Summary Comparison
| Storage Type | Storage Location | Value Buffer Content | Performance |
|---|---|---|---|
| Small Struct (≤24B) | Stack (Inside Container) | Actual data payload | High (No heap overhead) |
| Large Struct (>24B) | Heap | Pointer to heap memory | Medium (Heap overhead) |
| Class (Reference) | Heap | Pointer to heap object | Medium (ARC overhead) |
Why does this matter?
Understanding this mechanism helps you optimize Swift performance: Frequent passing of large structures within Any or protocol types can lead to unexpected heap allocation overhead. If performance is a critical requirement, it is recommended to use Generics. Generics allow for "Specialization" during compilation, which avoids the overhead of the expensive Existential Container entirely.