Existential container - это структура данных в рантайме, которая хранит значение типа, скрытого за протоколом. Он появляется там, где мы используем название протокола в качестве типа переменной или аргумента функции. С помощью existential container реализован динамический вызов методов протокола, а также управление жизненным циклом внутренного значения типа.
Здесь и далее мы рассмотрим 64-х битную архитектуру, где 1 машинное слово - это 8 байт. Existential container занимает 5 машинных слов или 40 байт и состоит из трех компонентов:
24 байта для данных
Указатель размером в 8 байт на меданные типа
Указатель размером в 8 байт на protocol witness table
Рассмотрим каждый компонент отдельно начиная с данных. Если экземпляр занимает меньше или ровно 24 байта, тогда все поля помещаются внутрь existential container, как, например, структура из 3-х полей типа Int.
struct InlineExample: Existential {
let id = 0xA
let hash: Int = 0xB
let sum: Int = 0xC
}
В нашем примере памяти existential container для этой структуры будет выглядеть так:

В первых трех адресах мы видим значения трех полей структуры. Это пример inline existential container, когда данные размещаются внутри контейнера.
Если структура занимает больше 24 байтов, тогда в начале existential container будет лежать указатель на специальный объект в куче. Этот объект состоит из указателя на метаданные, счетчика ссылок и значений всех полей этой структуры. По сути это и есть обычный reference type object. Но благодаря existential container при работе со значением сохраняется верная семантика копирования. Рассмотрим структуру с пятью полями, которая занимает больше 3 байт.
struct OutlineExample: Existential {
let id: Int = 0xA
let hash: Int = 0xB
let sum: Int = 0xC
let age: Int = 0xD
let code: Int = 0xE
}
Для нее будет создан existential container в первом бите которого будет указатель на объект с данными. Такой existential container называется outline existential container и в памяти выглядит так:

В первом байте лежит указатель на объект в куче, который выглядит так:

Мы разобрались с тем, где и как хранятся поля структуры внутри existential container. Далее перейдем к следующему компоненту контейнера - к метаданным типа и value witness table.
Value witness table
Посмотрим на значение в 4-м байте - здесь лежит указатель на метаданные типа и в одном из полей интересующая нас структура Value witness table (VWT). С ее помощью описывается, как работать со значениями конкретного типа на низком уровне - как создавать, копировать, уничтожать значения и какой размер у этого типа.
VWT состоит из следующих полей, мы рассмотрим только первые 6 из них.
Указатель на функцию инициализации
Указатель на функцию деинициализации
Указатель на функцию инициализации из копии initializeWithCopy
Указатель на функцию присваивания с копированием assignWithCopy
Указатель на функцию инициализации с переносом initializeWithTake
Указатель на функцию присваивания с переносом assignWithTake
Указатель на функцию получения тега из перечисления (enum) getEnumTagSinglePayload
Указатель на функцию для сохранения тега StoreEnumTagSinglePayload
Размер необходимый для хранения одного объекта этого типа Size
Размер каждого элемента этого типа в масиве Stride
Служебные флаги
Кол-во дополнительных полей в типе ExtraInhabitantCount
Общий смысл всех этих функций в выполнении каких-то операций в рантайме. Например, в примере кода для копирования переменной fixed в another компилятор генерирует вызов рантайм функция initializeBufferWithCopyOfBuffer
.
struct Fixed: Existential {
var num: Int
var arr: [Int]
}
func copy(fixed: Existential) {
let another = fixed
}
Она принимает три аргумента: existential container в который копировать, из которого копировать и указатель на метаданные типа, а возвращает указатель на existential container с новыми данными. Внутри функции данные из одного existential container копируются в другой.
Совсем другое дело если внутри inline existential container лежат примитивные типы данных, как у структуры ниже:
struct FixedInline: Existential {
var num: Int
var num1: Int
var num2: Int
}
func primitiveCopy(fixed: Existential) {
let another = fixed
}
Здесь для копирования используется один из вариантов функции __swift_memcpy
. Она принимает такие же аргументы и копирует поля из одного контейнера в другой. Например, для структуры из 3 полей компилятор создаст функцию с названием __swift_memcpy24_8
, где 24 - размер структуры в битах, а 8 - выравнивание данных.
В процессе деинициализации existential container вызывается функция из value witness table. Обозначим эту функцию как destroy
. В зависимости от конкретного типа внутри existential container будет сгенерирована соответствующая реализация этой функции. Рассмотрим еще раз такую структуру:
struct Fixed: Existential {
var num: Int
var arr: [Int]
}
func copy(fixed: Existential) {
let another = fixed
}
Здесь в конце функции copy
компилятор генерирует вызов destroy из value witness table для типа FixedInline. Функция принимает два аргумента - объект для деинициализации и метаданные типа этого объекта. Внутри этого варианта функции destroy будем вызван деинициализатор для массива в поле arr
. Это пример того, как деициниализируется inline existential container содержащий хотя бы один нетривиальный тип.
Если внутри inline контейнера все типы тривиальные, то будет сгенерирована и вызвана пустая функция-заглушка __swift_noop_void_return
, Например, для такой структуры
struct FixedTrivial: Existential {
var num: Double
var index: Int
var primary: Int
var secondary: Int
}
Здесь компилятору ничего не нужно делать, ведь все поля хранятся в стеке и у них нет своих деинициализаторов.
Немного иначе дела обстоят с noninline existential container. Здесь для деинициализации вызывается функция swift_release
, которая уменьшает счетчик ссылок внутри объекта с полями в куче на единицу.
struct FixedTrivial: Existential {
var num: Double
var index: Int
var primary: Int
var secondary: Int
}
Мы посмотрели на несколько вариантов деинициализации existential container и далее перейдем к следующей функции initializeWithCopy
из value witness table. Рассмотрим такой пример:
func testInitWithCopy() {
var test: TestExternalProto = ResilientWeakRef(Referent())
var testOne = test
}
Структура ResilientWeakRef
определена в другом модуле, который собран в режиме library evolution. В этом режиме публичный тип ResilientWeakRef
считается устойчивым (resilient) и компилятор не может сгенерировать код прямой инициализации. Вместо этого он генерирует вызов initializeWithCopy
из VWT конкретного типа. Эта функция принимает destination existential container, source existential container и метаданные типа в качестве аргументов и копирует один existential container в другой.
struct Test: TestExternalProto {
var ref: MyTestFramework.Referent?
var num: Int = 5
var double: Double = 2.0
var str: String = "Test string"
}
func testInitWithTake(test: inout TestExternalProto) {
var existential: any TestExternalProto = Test()
existential = test
}
Функция initializeWithTake
вызывается для создания нового existential container при помощи уже существующего.
Компилятор генерирует функцию assignWithTake
если есть хотя бы одно поле нетривиального типа. Она перемещает значение из destination existential container в source existential container. После вызова этой функции destination existential container остается в невалидном состоянии и его больше нельзя использовать. По сути это move семантика внутри Swift.
Где и когда будет сгенерирован оставшийся из всех вызов assignWithCopy
я так и не смог понять. Либо я что-то упустил, либо компилятор уже не генерирует ее вызов.
Мы рассмотрели разные функции из value witness table, обсудили разные варианты инициализации и деинициализации existential container и копирования одного в другой. Теперь познакомимся со следующим компонентом в existential container - с protocol witness table.
Protocol witness table
Protocol witness table (PWT) - это рантайм структура данных, которая содержит указатели на witness function - то есть на реализацию конкретного метода из протокола. Компилятор генерирует protocol witness table для каждой структуры, класса или перечисления, которые реализуют хотя бы один протокол. Например, для структуры Test, которая реализует протокол Existential компилятор сгенерирует protocol witness table:
protocol Existential {
func firstFunction()
func secondFunction()
func thirdFunction()
}
struct Test: Existential {
func firstFunction() { }
func secondFunction() { }
func thirdFunction() { }
}
В памяти protocol witness table для Existential у типа Test будет выглядеть так:

В первом поле лежит protocol conformance descriptor. Мы не будем глубоко рассматривать эту сущность. Нас больше интересуют указатели на реализацию (witness function) конкретных методов.
Для вызова нужного метода протокола компилятор вычислит смещение указателя на этот метод относительно начала protocol witness table и сгенерирует вызов соответствующей реализации. Так реализовал вызов метода протокола у всех соответствующих этому протоколу типов - это virtual table для протоколов.
Далее коротко сравним реализацию generics и protocol witness table.
Отличия от дженериков
Generics, в отличии от existential container, в теории использую статическую диспетчеризацию методов. Тип дженерика известен во время компиляции, тогда как конкретный тип в existential container скрыт от нас.
Для дженерик функций компилятор генерирует конкретную реализацию метода для разных типов. Это происходит только при сборке с оптимизацией по скорости (-O) и если функция не встроенная (non inline).
В тоже время комплиятор не знает про конкретный тип внутри existential container и будет использовать PWT для динамического вызова методов протокола. То есть existential container по своей сути динамический.
Existential container требует выделения памяти в куче, если размер значения больше 3-х байт. Для дженериков такой необходимости нет, если компилятор может сгенерировать реализацию функции для разных типов (specialized implementation). Но это в свою очередь может привести к увеличению размера бинарного файла.
Компилятор также может использовать value buffer и PWT для неспециализированных дженериков. В такой случае поведение будет похоже на existential container.
Выводы
Мы разобрались для чего нужен existential container и как он выглядит в памяти. Затем познакомились с value witness table и разобрали несколько функций из нее. Далее коротко обсудили protocol witness table и диспетчеризацию методов протокола. В конце разобрали отличия generics от existential container. Рекомендуется использовать дженерики по умолчанию, если нет необходимости в динамическом определении типа. Например, в массиве хранить конкретный тип, а не тип протокола. Надеюсь, что эта статья поможет глубже разобраться в природе existential container.
Bardakan
В swift есть механизм copy-on-write, и на Хабре есть отдельная статья о том, что структуры при обычном присваивании переменной НЕ копируются. Ссылочные типы по умолчанию не копируются вообще - вам нужно принудительно создавать их копию.
Тогда откуда у вас взялись какие-то там copy-подобные вызовы?