Swift. Struct vs Class memory and performance comparison или следует ли всегда использовать Struct

3e645f417f4bc4191811ca0d4ba8b67e

Проблема

Как и многие 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(of o: UnsafePointer) -> String {
	String(format: "%p", Int(bitPattern: o))
}

func address(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 и соблюдать принципы, а время доступа незначительно отличается, я бы советовал использовать классы и протоколы для построения архитектуры.

© Habrahabr.ru