Copy-on-write
Познакомился я с этой темой на одном из собеседований, когда мне задали вопрос о том, что есть какая-то большая структура и сколько памяти будет занято, если переменную с экземпляром этой структуры присвоить другой переменной. Тогда, вспоминая о том, что происходит при взятие подстроки, я ответил, что объем памяти не изменится и ошибся. Ну и уже после собеседования полез читать о copy-on-write (далее COW).
В этой статье мы обсудим эту тему и она станет очередным пунктом в том, чем отличаются ссылочные типы от типов значений в языке Swift. Как и говорилось выше, одним из таких отличий является наличие функционала COW, о котором и будем говорить.
Как известно основным отличием ссылочных типов от типов значений является то, что первые передаются по ссылкам, в то время как вторые копируются. У этого есть ряд своих плюсов таких как то, что типы значений работают быстрее, потому что хранятся на стеке, а не в куче, используют статическую диспетчеризацию и прочее. Однако можно задаться вопросом «а зачем копировать данные, если мы их не меняем?». Действительно и COW как раз-таки отвечает на этот вопрос и говорит, что это не обязательно.
Проведем небольшой эксперимент:
func address(o: UnsafeRawPointer) {
print("address: \(Int(bitPattern: o))")
}
var arr1 = [1, 2, 3]
var arr2 = arr1
address(arr1)
address(arr2)
Как думаете, что выведет?
А выведет то, чего мы и хотели, задавая вопрос:
address: 105553133388704
address: 105553133388704
Обе переменные ссылаются на одну область памяти, хотя мы и работаем с типом значений и данные первой переменной должны были бы копироваться во вторую.
А теперь изменим второй массив и посмотрим что будет.
func address(o: UnsafeRawPointer) {
print("address: \(Int(bitPattern: o))")
}
var arr1 = [1, 2, 3]
var arr2 = arr1
arr2.append(4)
address(arr1)
address(arr2)
address: 105553154755744
address: 105553135788672
Как можно заметить теперь ссылки ведут на разные области в памяти.
Что нам это дает?
Благодаря этим экспериментам становится ясно, что swift предоставляет механизм, который копирует поведение ссылочных типов на типы значений до первых изменений, такой механизм и называется COW. Это избавляет нашу программу от лишних копирований, тем самым улучшая производительность.
Увы, но даже не для всех структур из стандартной библиотеки он реализован, но точно реализован для основных типов, которые при копирование могут затратить большое количество ресурсов, а именно: String, Array, Dictionary и Set.
Становится очевидно, что для наших с вами собственных структур такой механизм тем более не предусмотрен, но — не беда, мы можем сделать его сами.
Собственный COW
final class Ref {
var val: T
init(v: T) {
val = v
}
}
struct Box {
var ref: Ref
init(x: T) {
ref = Ref(x)
}
var value: T {
get {
ref.val
}
set {
if (!isKnownUniquelyReferenced(&ref)) {
ref = Ref(newValue)
} else {
ref.val = newValue
}
}
}
}
Одним из важных моментов этого кода является метод:
func isKnownUniquelyReferenced (_ object: inout T?) -> Bool where T : AnyObject
Returns a Boolean value indicating whether the given object is known to have a single strong reference.
Данная реализация хранит наше value (в Box) одинаковым при переиспользование. Посмотрим на примере как этим пользоваться и каков будет результат. Для этого создадим нашу собственную структуру Test и один ее экземпляр.
struct Test {
var x: Int
}
let val = Test(x: 5)
// Ну и используем наш Box
var box = Box(val)
var box2 = box
address(&box.value)
address(&box2.value)
Как и планировалось, при выводе мы получим один и тот же адрес:
address: 140701847686488
address: 140701847686488