Проблема

Как и многие 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

-

+

+

+

  1. Использовать структуры с использованием протоколов плохо для Stack, но хорошо для скорости выполнения.

  2. Использовать классы хорошо для Stack.

Так как важно следить за Stack и соблюдать принципы, а время доступа незначительно отличается, я бы советовал использовать классы и протоколы для построения архитектуры.

Комментарии (6)


  1. varton86
    10.06.2022 16:51

    Структура StructForProperty имеет в своем составе переменную ссылочного типа, поэтому ничего удивительного.


  1. house2008
    10.06.2022 17:28
    +1

    А зачем экономить стек ?) То есть дизайнеры свифта специально придумали структуры чтобы как можно больше хранить в стэке, а вы предлагаете от него отказаться... Это как поставить в комп 60 Гб оперативы и использовать только 6 Гб, а остальное свапать на диск. Да, размер стэка влияет на размер бинарника, но не считаю это весомым аргументом отказываться от структур.

    Еще можно рассказать про release/retain для полей структур и классов и какой оверхед рантайма происходит при передачи этих объектов )


    1. astec
      11.06.2022 20:07

      размер стэка влияет на размер бинарника

      Это где? И почему?


      1. 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 мб, но не могу найти ссылку.


  1. house2008
    10.06.2022 17:35

    Использовать структуры с использованием протоколов плохо для Stack, но хорошо для скорости выполнения.

    возможно callsite действительно будет быстрым, но сама инжекция объекта по коду будет медленной из-за release/retain и чем больше reference полей будет у структуры тем существенней будет проседать райнтайм.


    1. tonybragin Автор
      10.06.2022 19:29

      Интересные замечания, буду углубляться)