Swift использует оптимизацию компилятора, известную как использование дополнительных существителей (или упаковка запасных битов), чтобы устранить накладные расходы по хранилищу для случая none в Optional. Для ссылочных типов (классы, замыкания, AnyObject) представление указателя включает нулевой адрес (0x0), который не является допустимой ссылкой на объект; Swift переназначает этот нулевой указатель для представления Optional.none, в то время как все ненулевые указатели представляют Optional.some. При расширении этого механизма на общие перечисления с несколькими случаями, несущими полезные нагрузки, компилятор анализирует битовые шаблоны всех типов ассоциированных значений, чтобы определить общие неиспользуемые значения (запасные биты). Если все типы полезной нагрузки имеют достаточное количество запасных битов для кодирования количества случаев, перечисление хранит дискриминатор случаев в этих битах; в противном случае оно добавляет отдельный байт или слово-тег.
При проектировании сцены для движка реального времени 3D рендеринга команде необходимо было хранить необязательные ссылки на родителя для 2 миллионов узлов сцены. Каждый узел был экземпляром класса, и иерархия требовала Optional<Node>, чтобы представлять корневые узлы (которые не имеют родителя).
Решение A: Параллельный булевый массив.
Команда рассмотрела возможность поддерживания отдельного ContiguousArray<Bool> вместе с ContiguousArray<Node>, чтобы указывать на присутствие родителя.
Плюсы: Явный контроль, язык-независимая модель.
Минусы: Локальность кеша нарушается из-за доступа к двум разным областям памяти; накладные расходы по памяти увеличились на 2 МБ (1 байт на булевое значение, дополненный до выравнивания); сложность синхронизации при перестройке дерева.
Решение B: Шаблон узла-сентинела.
Использование глобального единственного экземпляра "нулевого узла" для представления отсутствующих родителей.
Плюсы: Хранение единственного указателя, отсутствие накладных расходов на опциональные значения.
Минусы: Нарушает безопасность типов; компилятор не может предотвратить случайные операции с сентинелом; требуется защитная проверка по всему коду; вводит циклы ссылок, если сентинел хранит ссылки на реальные узлы.
Решение C: Нативный Swift Optional.
Прямое использование Optional<Node> внутри структуры узла.
Плюсы: Полная безопасность на этапе компиляции, идиоматичный синтаксис Swift, нулевые накладные расходы на память, поскольку Optional использует представление нулевого указателя для none.
Минусы: Требует понимания того, что эта оптимизация применяется специально к ссылочным типам; типы значений, такие как Int, подвергнуты дополнению.
Команда выбрала Решение C. Поскольку Node был классом, обертка Optional не добавила байтов к размеру экземпляра. В результате произошло снижение потребления памяти примерно на 16 МБ по сравнению с параллельным булевым подходом (устранение как хранения булевых значений, так и связанных выравнивающих дополнений), при этом были получены гарантии на этапе компиляции, которые устранили целый класс сбоев из-за разыменования нулевых указателей во время последующего рефакторинга.
Почему Optional<Int> обычно занимает больше памяти, чем Int, в то время как Optional<AnyObject> занимает то же пространство, что и AnyObject?
Int является 64-битным целым числом в формате дополнительного кода, использующим все возможные битовые шаблоны для представления своего числового диапазона (-2^63 до 2^63-1), не оставляя недопустимых битовых шаблонов (дополнительных существителей) для дискриминанта Optional. Следовательно, компилятор должен добавить отдельный байт (или слово, в зависимости от выравнивания), чтобы хранить, является ли опциональное значение some или none. Напротив, AnyObject (и все ссылочные типы) являются указателями, где все нулевое битовое представление (null) гарантированно является недопустимым адресом объекта; Optional использует это нулевое представление для своего случая none, требуя нулевого дополнительного хранилища.
Сколько различных представлений на уровне машины существует для "отсутствия" в Optional<Optional<T>>, когда T является классом, и почему это важно для равенства?
Существуют два различных представления: внешний .none (нулевой указатель на внешнем уровне) и .some(.none) (действительный внешний указатель, указывающий на внутренний null). Поскольку внутренний Optional уже использует значение нулевого указателя для представления своей собственной пустоты, внешний Optional не может отличить свой собственный none от .some, содержащего внутренний none, только по значению указателя. Следовательно, внешнему уровню требуется отдельный бит-тег, и два концептуальных состояния "nil" не равны (Optional(Optional.none) != Optional.none). Это различие имеет важное значение при вложении опционалов, возвращаемых из обобщенных API или JSON декодирования, где отсутствующие ключи производят внешние nil-значения, а нулевые значения производят внутренние nil-значения.
Что определяет, будет ли компилятор хранить отдельный байт-тег или встраивать дискриминатор случая внутри полезной нагрузки при определении перечисления с несколькими случаями полезной нагрузки, такими как case integer(Int), case boolean(Bool)?
Компилятор выполняет анализ запасных битов по типам ассоциаций. Bool использует только младший значащий бит, оставляя 7 битов в запасе. Если все полезные нагрузки случаев предоставляют достаточное количество запасных битов, чтобы уникально идентифицировать каждый случай (например, несколько ссылок на классы, объединяющих нулевой дополнительный существитель), перечисление может упаковать индекс случая в эти неиспользуемые биты. Однако Int и Bool имеют раздельные шаблоны запасных битов (Int не имеет), что заставляет компилятор выделять отдельный байт-тег (или слово), чтобы отличить integer от boolean, увеличивая размер перечисления за пределами максимального размера полезной нагрузки.