Copy-on-write

ddd84894769f8de0b5a7bbf486e8e3ea

Познакомился я с этой темой на одном из собеседований, когда мне задали вопрос о том, что есть какая-то большая структура и сколько памяти будет занято, если переменную с экземпляром этой структуры присвоить другой переменной. Тогда, вспоминая о том, что происходит при взятие подстроки, я ответил, что объем памяти не изменится и ошибся. Ну и уже после собеседования полез читать о 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

Источники

© Habrahabr.ru