Проблема
Как и многие iOS разработчики, я столкнулся с дилеммой: какой объект использовать для построения архитектуры проекта. Взять для примера реализацию паттерна фасад. Этот объект должен принять некоторое количество сущностей и реализовать методы для упрощенного доступа к ним. Если не вдаваться в подробности, то подойдет и класс, и структура: оба могут инкапсулировать объекты и функции. Так что же выбрать?
А что говорит Apple?
Если обратиться к статье на сайте Apple Choosing Between Structures and Classes, то следуя пункту "Use structures by default.", нужно использовать структуры в нашем случае. Если конечно не нужна Objective-C совместимость.
Тестовые объекты
Объекты наполнения для целевых объектов:
protocol ObjectForProperties {
var queue: DispatchQueue { get } // reference type
var bool: Bool { get } // value type
}
final class ClassForProperty: ObjectForProperties {
var queue: DispatchQueue = .main
var bool: Bool = true
}
struct StructForProperty: ObjectForProperties {
var queue: DispatchQueue = .main
var bool: Bool = true
}
var classForProperty = ClassForProperty() // reference type
var structForProperty = StructForProperty() // value type
var array = [1] // value type with Copy-on-Write
Целевые объекты:
// Используем класс без использования протоколов
final class UseClassWithoutProtocols {
var ref: ClassForProperty
var value: StructForProperty
var array: [Int]
init(ref: ClassForProperty, value: StructForProperty, array: [Int]) {
self.ref = ref
self.value = value
self.array = array
}
}
// Используем класс с использованием протоколов
final class UseClassWithProtocols {
var ref: ObjectForProperties
var value: ObjectForProperties
var array: [Int]
init(ref: ObjectForProperties, value: ObjectForProperties, array: [Int]) {
self.ref = ref
self.value = value
self.array = array
}
}
// Используем структуру без использования протоколов
struct UseStructWithoutProtocols {
var ref: ClassForProperty
var value: StructForProperty
var array: [Int]
}
// Используем структуру с использованием протоколов
struct UseStructWithProtocols {
var ref: ObjectForProperties
var value: ObjectForProperties
var array: [Int]
}
let useClassWithoutProtocols = UseClassWithoutProtocols(
ref: classForProperty,
value: structForProperty,
array: array
)
let useClassWithProtocols = UseClassWithProtocols(
ref: classForProperty,
value: structForProperty,
array: array
)
var useStructWithoutProtocols = UseStructWithoutProtocols(
ref: classForProperty,
value: structForProperty,
array: array
)
var useStructWithProtocols = UseStructWithProtocols(
ref: classForProperty,
value: structForProperty,
array: array
)
Размещение в памяти объектов
Структуры передаются копированием значения. У классов вместо копирования используется ссылка на существующий экземпляр. Для некоторых стандартных типов значения реализован механизм Copy-on-Write, который позволяет передавать объект передавая адрес и копировать только если объект изменяется. Но Copy-on-Write не реализован у структур по-умолчанию, то есть StructForProperty будет передаваться копированием в любом случае.
Проверим размещение объектов в памяти (опустим закрытые протоколом значения для чистоты эксперимента):
func address<T>(of o: UnsafePointer<T>) -> String {
String(format: "%p", Int(bitPattern: o))
}
func address<T: AnyObject>(of classInstance: T) -> String {
String(format: "%p", unsafeBitCast(classInstance, to: Int.self))
}
// classForProperty addresses
print(address(of: classForProperty)) // 0x6000020e9600
print(address(of: useClassWithoutProtocols.ref)) // 0x6000020e9600
print(address(of: useStructWithoutProtocols.ref)) // 0x6000020e9600
// structForProperty addresses
print(address(of: &structForProperty)) // 0x111a1bca8
print(address(of: &useClassWithoutProtocols.value)) // 0x600002edb6d8
print(address(of: &useStructWithoutProtocols.value)) // 0x111a1bcd8
// array addresses
print(address(of: &array)) // 0x600002edb2f0
print(address(of: &useClassWithoutProtocols.array)) // 0x600002edb2f0
print(address(of: &useStructWithoutProtocols.array)) // 0x600002edb2f0
Что и требовалось доказать:
Ссылочный объект имеет только один экземпляр в памяти.
Тип значения имеет столько экземпляров в памяти, сколько раз он передавался.
Тип значения с Copy-on-Write имеет только один экземпляр в памяти.
Занимаемая память
В большинстве случаев структуры полностью хранятся в Stack, а классы хранятся в Heap, и ссылка на объект хранится в Stack. Stack имеет ограниченный размер. Heap не имеет ограничений по размеру. Из чего следует вывод, что память в Stack нужно беречь. Большая статья про память.
MemoryLayout.size(ofValue: useClassWithoutProtocols) // Размер в Stack 8
class_getInstanceSize(UseClassWithoutProtocols.self) // Общий размер 48
MemoryLayout.size(ofValue: useClassWithProtocols) // Размер в Stack 8
class_getInstanceSize(UseClassWithProtocols.self) // Общий размер 104
MemoryLayout.size(ofValue: useStructWithoutProtocols) // Размер в Stack 32
MemoryLayout.size(ofValue: useStructWithProtocols) // Размер в Stack 88
Если беречь память Stack, то очевидно нужно использовать классы. Если брать общий размер, то оптимальным решением будет использование структур без использования протоколов. Но в таком случае страдает тестируемость, принципы SOLID и конечно Протокольно-ориентированное программирование. Так же не стоит забывать про копирование: размер занимаемой память структурой прямо пропорционален количеству присваиваний объекта.
Время доступа
В теории время доступа к значениям класса дольше, так как переход по ссылкам требует большего времени. В то время как структуры находятся здесь и сейчас.
Замер получения булевой переменной в разных конфигурациях:
func countTime(_ workItem: () -> Void) -> Double {
let start = CFAbsoluteTimeGetCurrent()
workItem()
return CFAbsoluteTimeGetCurrent() - start
}
// для примера
// повторить для разных конфигураций
countTime {
let _ = useClassWithoutProtocols.ref.bool
}
Я провел несколько тестов и получил следующие средние значения:
Время доступа к ссылочному типу |
Время доступа к типу значения |
|
useClassWithoutProtocols |
0,000009597 |
0,000007105 |
useClassWithProtocols |
0,000009501 |
0,000008715 |
useStructWithoutProtocols |
0,000008059 |
0,000008597 |
useStructWithProtocols |
0,000007011 |
0,000007714 |
Значения примерно равнозначны. Однако стоит отметить, что использования структуры со значениями ссылочных типов, закрытых протоколами, незначительно экономит время доступа.
Выводы
Stack |
Heap |
Время |
Принципы |
|
useClassWithoutProtocols |
+ |
- |
- |
- |
useClassWithProtocols |
+ |
- |
+ |
|
useStructWithoutProtocols |
- |
+ |
_ |
|
useStructWithProtocols |
- |
+ |
+ |
+ |
Использовать структуры с использованием протоколов плохо для Stack, но хорошо для скорости выполнения.
Использовать классы хорошо для Stack.
Так как важно следить за Stack и соблюдать принципы, а время доступа незначительно отличается, я бы советовал использовать классы и протоколы для построения архитектуры.
Комментарии (6)
house2008
10.06.2022 17:28+1А зачем экономить стек ?) То есть дизайнеры свифта специально придумали структуры чтобы как можно больше хранить в стэке, а вы предлагаете от него отказаться... Это как поставить в комп 60 Гб оперативы и использовать только 6 Гб, а остальное свапать на диск. Да, размер стэка влияет на размер бинарника, но не считаю это весомым аргументом отказываться от структур.
Еще можно рассказать про release/retain для полей структур и классов и какой оверхед рантайма происходит при передачи этих объектов )
astec
11.06.2022 20:07размер стэка влияет на размер бинарника
Это где? И почему?
house2008
13.06.2022 10:26Ведь скорость работы структур достигается не какой-то магической силой) Моё понимание, что компилятор разворачивает структуры на стеке, это дополнительные инструкции, которые будут сохранены в data (или text) сегменте бинарника. То есть вместо хранения одной reference переменной на инстанс класса в heap, в функцию будет скопирована вся структуру со всеми полями. https://stackoverflow.com/questions/52372423/why-does-usage-of-structs-increase-applications-binary-size и https://forums.swift.org/t/how-do-structs-increase-binary-size/35208. Получается аналогия с инлайнингом функций, которые тоже увеличивают размер приложения.
Где-то видел статью, что человек переделал несколько сотен структур на классы и размер приложения уменьшился на 13 мб, но не могу найти ссылку.
house2008
10.06.2022 17:35Использовать структуры с использованием протоколов плохо для Stack, но хорошо для скорости выполнения.
возможно callsite действительно будет быстрым, но сама инжекция объекта по коду будет медленной из-за release/retain и чем больше reference полей будет у структуры тем существенней будет проседать райнтайм.
varton86
Структура StructForProperty имеет в своем составе переменную ссылочного типа, поэтому ничего удивительного.