Управление памятью в Swift

Если на собеседовании вам не задавали вопросы про управление памятью, то уверяю вас, это дело времени. Что же вообще такое память? Это длинная последовательность байтов (если кто-то забыл, то 1 байт = 8 бит). Байты расположены упорядоченно, каждый байт имеет свой адрес в памяти. Когда мы создаем Value или Reference Type, то под наши объекты выделяется память (те самые упорядоченные байты). В этой статье мы будем учиться определять свойства объекта в памяти с помощью MemoryLayout.

Терминология

При выделении памяти программа используется две области в оперативной памяти — стек(stack) и кучу(heap). Про разницу между стеком и кучей можно почитать в этой статье: чем стек отличается от кучи? Ниже приведу лишь краткий перечень:

Стек:

  • Переменные, выделенные в стеке хранятся непосредственно в памяти, доступ к этой памяти очень быстрый. Выделение этой памяти происходит во время компиляции программы;
  • Используется для статического выделения памяти. Организована по принципы LIFO, то есть последним пришел, первым вышел;
  • Есть ограничение — данные, которые хранятся в стеке должны быть статичными и конечными. Это значит, что на этапе компиляции их размер должен быть известен;
  • Каждый поток в приложении имеет доступ к своему собственному стеку.

Куча:

  • Используется для динамического выделения памяти;
  • Операции происходят медленнее, чем в стеке, так как требуются дополнительные операции, чтобы найти данные в куче;
  • Куча общая для всех потоков приложения.

Очень важно знать и понимать сколько памяти занимает создаваемый вами объект. И мы можем узнать это с помощью встроенного типа перечисления MemoryLayout.

Перед тем, как мы рассмотрим MemoryLayout, давайте введем терминологию:

  • Бит — наименьшая единица измерения количества информации. Байт — вторая по величине единица измерения. 1 байт = 8 бит;
  • Size (размер) — количество байтов для хранения типа в памяти;
  • Alignment (выравнивание) — способ упорядочения данных и доступа к ним в памяти;
  • Stride (шаг) — количество байтов между двумя элементами в памяти;
  • Type (тип) — представление типа данных, например, Integer или Bool.

Мы можем узнать о расположении памяти типов Swift, используя MemoryLayout. Это дает нам информацию об объекте в памяти, например, размер, выравнивание и шаг рассматриваемого типа. Шаг типа всегда должен быть больше или равен его размеру.

Рассчитываем размер памяти (Size)

MemoryLayout(T).size означает нечто иное, чем sizeof в Objective-C. Swift показывает нам количество байтов, используемых для хранения типа во время компиляции.

Давайте рассмотрим пример того, как можно получить размер типа с помощью MemoryLayout:

MemoryLayout<String>.size // 16 байт
MemoryLayout<Int>.size // 8 байт
MemoryLayout<Int32>.size // 4 байта
MemoryLayout<Int16>.size // 2 байта
MemoryLayout<Bool>.size // 1 байт

В результате мы видим, что размер типа String занимает 16 байт, Int — 8 байт, Int32 — 4 байта, Int16 — 2 байта, а Bool — 1 байт. Вроде все просто. Давайте усложним пример.

Рассмотрим следующую структуру:

struct TestStruct {
    var one: Bool
    var two: Bool
}

Так как структура — тоже тип, значит мы можем узнать ее размер в памяти!

MemoryLayout<TestStruct>.size // 2 байта

Почему размер равен 2? Все просто, повторим: размер (size) — необходимое количество Байтов для хранения типа в памяти. Так как тип Bool занимает в памяти 1 байт, то логично предположить, что структура из двух типов Bool занимает 2 байта. Отсюда можно скорректировать определение размера: размер типа — сумма всех его полей.

Вроде с этим разобрались. Давайте усложнять пример:

struct TestStruct {
    let one: String
    let two: Int
    let three: Bool
}

MemoryLayout<TestStruct>.size // 25 байт

Выше мы уже определили размер каждого из этих типов в структуре. Сумма всех размеров будет равна 25 байт. Давайте изменим порядок переменных в нашей структуре (переместим Bool в самое начало):

struct TestStruct {
    let three: Bool
    let one: String
    let two: Int
}

MemoryLayout<TestStruct>.size // 32 байта

Почему размер увеличился? В данном примере размер переменной three<Bool> занял 8 байт, one<String> — как и раньше, 16 байт, two<Int> — как и раньше, 8 байт. Чтобы понять, почему three занимает теперь 8, а не 1 байт, давайте рассмотрим следующие свойства — Stride(шаг).

Рассчитываем шаг в блоках памяти (Stride)

Как мы уже говорили ранее, шаг представляет собой расстояние между двумя объектами в памяти. Давайте рассмотрим следующий пример:

struct TestStruct {
    var one: Int32
    var two: Bool
}

MemoryLayout<TestStruct>.size // 5 байт
MemoryLayout<TestStruct>.stride // 8 байт

С размером, думаю, все понятно: Int32 занимает 4 байта, а Bool — 1 байт, сумма этих типов и дает результат в 5 байт. Но что такое на самом деле шаг? Давайте разбираться.

Если у нас будет две структуры TestStuct, то картинка с блоками памяти будет следующая:

iOS Interview Question. Структура MemoryLayout. Пример

Благодаря шагу мы знаем, на сколько байтов нужно двигать указатель, чтобы добраться до следующего объекта. Как мы видим, между первой и второй структурой находится 3 неиспользуемых байта. Причиной этому является последнее наше свойство — Alignment (выравнивание).

Рассчитываем выравнивание в блоках памяти (Alignment)

Суть выравнивания заключается в том, чтобы оптимизировать блоки памяти таким образом, чтобы к ним было как можно меньше обращений. Это ускорит программу. Если бы в нашем примере блоки памяти шли друг за другом, то есть на шестом байте начинался бы блок с Int32, то программе было бы сложно понять, где заканчивается первая структура и где начинается вторая. Для этого потребовалось бы больше операций.

В Swift у всех типов есть свои выравнивания:

  • String и Int выравниваются по 8 байт;
  • Int32 по 4 байта;
  • Int16 по 2 байта;
  • Bool по 1 байту.
MemoryLayout<Int>.alignment // 8 байт
MemoryLayout<Int32>.alignment // 4 байта
MemoryLayout<Int16>.alignment // 2 байта
MemoryLayout<Bool>.alignment // 1 байт

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

Разбираем примеры

Давайте рассматривать примеры и разбираться, что же из себя представляет управление памятью.

Пример №1

Начнем с простого:

struct TestStruct {
    var one: Bool
    var two: Bool
}

MemoryLayout<TestStruct>.size // 2 байта
MemoryLayout<TestStruct>.stride // 2 байта
MemoryLayout<TestStruct>.alignment // 1 байт

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

iOS Interview Question. Байты. Пример
  • Размер: размер структуры равен сумме двух типов Bool, каждый из которых равен 1 байту;
  • Выравнивание: так как структура состоит только из типов Bool, выравнивание которых равно 1, то и выравнивание будет равно 1 байту;
  • Шаг: так как выравнивание равно 1 байту, а размер структуры 2 байтам, то вторую структуру мы сможем расположить в памяти следом за первой (выравнивание = размеру, округленному в большую сторону, кратному выравниванию).

Выравнивание — это длина байта, по которому мы выравниваемся, то есть по 1 байту (поскольку самый большой элемент нашей структуры имеет длину 1 байт).

Пример №2

struct TestStruct {
    var one: Int32
    var two: Bool
}

MemoryLayout<TestStruct>.size // 5 байт
MemoryLayout<TestStruct>.stride // 8 байт
MemoryLayout<TestStruct>.alignment // 4 байта
iOS Interview Question. Блоки памяти. Пример
  • Размер: Int32 = 4 байта, а Bool = 1 байт. Суммируем, получаем результат;
  • Выравнивание: Берем наибольшее выравнивание из всех свойств — Int32;
  • Шаг: Так как размер нашей структуры равен 5 байтам, то округляем размер в большую сторону к числу, кратному выравниваю, получаем 8 байт.

Из полученных нами знаний давайте упростим определения рассматриваемых свойств, касательно структур:

Размер (size) — это сумма размеров всех типов объекта (в нашем случае структуры);

Шаг (stride) — размер, округленный в большую сторону, кратный выравниванию;

Выравнивание (alignment) — это наибольшее выравнивание из всех свойств объекта (в нашем случае структуры).

Пример №3

Наконец, рассмотрим последний пример:

struct TestStruct {
    let one: Int
    let two: Bool
}

MemoryLayout<TestStruct>.size // 9 байт
MemoryLayout<TestStruct>.stride // 16 байт
MemoryLayout<TestStruct>.alignment // 8 байт
iOS Interview Question. Управление памятью. Пример
  • Размер: сумма Int и Bool дает значение 9 Байт;
  • Выравнивание: наибольшее выравнивание из свойств структуры = 8 Байт;
  • Шаг: так как размер равен 9, то наибольшее значение, кратное выравниванию = 16 Байт.

Давайте посмотрим как работает управление памятью и немного изменим этот же пример — поставим Bool на первое место:

struct TestStruct {
    let one: Bool
    let two: Int
}

MemoryLayout<TestStruct>.size // 16 байт
MemoryLayout<TestStruct>.stride // 16 байт
MemoryLayout<TestStruct>.alignment // 8 байт

Размер увеличился! Давайте разбираться.

iOS Interview Question. MemoryLayout. Пример
  • Размер: из-за того что Int имеет выравнивание равное 8, то Bool должен начинаться с байта, кратному 8, поэтому и образовывается пустое место между Bool и Int, что и влечет за собой увеличения размера структуры;
  • Выравнивание: как и раньше, наибольшее значение = 8 байт;
  • Шаг: так как размер кратен выравниванию, то и шаг остается равным 16 байт.

Рассчитываем размер класса

Попробуем определить размер класса с помощью MemoryLayout:

class TestClass {
    let one: Bool
    let two: Int

    init(oneValue: Bool, twoValue: Int) {
        one = oneValue
        two = twoValue
    }
}

MemoryLayout<TestClass>.size // 8 байт
MemoryLayout<TestClass>.stride // 8 байт
MemoryLayout<TestClass>.alignment // 8 байт

Для классов размер ссылки составляет 8 байт, поэтому все значения и будут равны 8 байт. Но не путайте: размер ссылки равен 8 байт, но не реальный размер объекта в куче.

Чтобы определить реальный размер объекта в куче, вы можете использовать Objective-C runtime — функцию class_getinstanceSize(_:), которая возвращает размер экземпляра класса.

Пример вызова функции:

class TestClass {
    let one: Bool
    let two: Int

    init(oneValue:Bool, twoValue: Int ) {
        one = oneValue
        two = twoValue
    }
}

class_getInstanceSize(TestClass.self)

Дополнительное чтение на тему Управление памятью:

Выразить благодарность или найти уникальный материал вы можете в boosty.

Подписывайтесь на мой Telegram-канал iOS Interview Channel, чтобы не пропустить новый материал.


4 thoughts on “Управление памятью в Swift”

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *