Познакомился я с этой темой на одном из собеседований, когда мне задали вопрос о том, что есть какая-то большая структура и сколько памяти будет занято, если переменную с экземпляром этой структуры присвоить другой переменной. Тогда, вспоминая о том, что происходит при взятие подстроки, я ответил, что объем памяти не изменится и ошибся. Ну и уже после собеседования полез читать о 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<T> { 
  var val: T
	
  init(v: T) { 
    val = v
  } 
}

struct Box<T> {
  var ref: Ref<T>
	
  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<T> (_ 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

Источники

Комментарии (7)


  1. MentalBlood
    25.06.2022 11:13

    Нечто похожее есть в Python


  1. Helltraitor
    26.06.2022 12:48
    +1

    На хабре есть хорошая статья про Cow в Rust Rust: От &str к Cow.

    Признаться, я раньше недоумевал, почему тип называется "корова"


    1. LyskinDan Автор
      26.06.2022 12:52

      Да, действительно, это нечто подобное, но это все же для rust, поэтому сложно наткнуться, когда ищешь именно для swift. Вообще идея написания была в том, что в русском сегменте мало информации именно для swift. Но механизм COW везде один, это правда


      1. ptr128
        27.06.2022 00:18
        +1

        Нет, не один. Вышеописанный механизм зависит от языка и накладывает ограничения, если требуется вызывать функции на других языках. Тогда как COW на уровне MMU (через mmap() с MAP_PRIVATE) универсален. Более того, так как он оперирует на уровне страниц виртуальной памяти, для больших объектов, в которых изменяется лишь незначительная часть данных, он существенно эффективней.


  1. varton86
    27.06.2022 13:06

    я ответил, что объем памяти не изменится и ошибся

    Обе переменные ссылаются на одну область памяти

    Т.е. ошибки всё-таки не было?


    1. LyskinDan Автор
      27.06.2022 15:06
      +1

      Была, так как у собственных структур нет механизма COW по дефолту, поэтому оно копировалось, а не ссылалось на одну область памяти


      1. varton86
        27.06.2022 15:29

        Да, забавно. При том, что если эту структуру поместить, например, в Array, то COW опять сработает.