Если на собеседовании вам не задавали вопросы про управление памятью, то уверяю вас, это дело времени. Что же вообще такое память? Это длинная последовательность байтов (если кто-то забыл, то 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, то картинка с блоками памяти будет следующая:
Благодаря шагу мы знаем, на сколько байтов нужно двигать указатель, чтобы добраться до следующего объекта. Как мы видим, между первой и второй структурой находится 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 байт
Представим, что у нас две такие структуры. Тогда их блоки памяти можно изобразить следующим образом:
- Размер: размер структуры равен сумме двух типов 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 байта
- Размер: 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 байт
- Размер: сумма 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 байт
Размер увеличился! Давайте разбираться.
- Размер: из-за того что 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)
Дополнительное чтение на тему Управление памятью:
- Value Type и Reference Type или чем стек отличается от кучи?
- Управление памятью в Swift
- Память в Swift от 0 до 1
Выразить благодарность или найти уникальный материал вы можете в boosty.
Подписывайтесь на мой Telegram-канал iOS Interview Channel, чтобы не пропустить новый материал.
пример 3, второй снипет — ошибка в объяснении.
Не могли бы пояснить, где именно ошибка? Пробежался по примеру, вроде все сходится
Выравнивание: как и раньше, наибольшее значение = 16 байт;
На скрине значение — 8 байт, тут ошибка
Все верно, поправил запись. Спасибо!