스위프트에서는 값타입을 최적화하기위해 COW를 사용한다
값타입이 복사될때 실제 복사가 일어난다면 너무 많은 메모리를 사용하게 되고, 성능이 저하될 수 있다.
이것을 최적화 하기 위해 복사될때는 원본 리소스를 공유하고 있다가, 값이 수정될 때 실제 복사가 일어나도록 하는 방법이다.
그리고 공식문서에 "Array, Set, Dictionary에는 COW가 구현되어 있습니다." 고 나와있다.
그런데 실험을 하는 중에 왜 Set과 Dictionary에는 COW가 적용되지 않을까? 라는 의문이 들어서 이 글을 적게 되었다.
본격적으로 실험을 하기 전에 같은 메모리주소를 가졌는지 확인해보기한 메서드를 정의했다.
아래 두 메서드는 객체의 메모리 주소를 출력해주는 메서드이다.
// 구조체용 메모리 주소 확인 메서드
func address(o1: UnsafeRawPointer) {
let address = String(format: "%p", Int(bitPattern: o1))
print(address)
}
// 클래스용 메모리주소 확인 메서드
func address<T: AnyObject>(o2: UnsafePointer<T>) {
let address = String(format: "%p", Int(bitPattern: o2))
print(o2.pointee) // 변수가 가르키는 타입
print(address) // 주소값
}
실험에 앞서 구조체를 하나 선언해주었다.
struct MyStruct {
var name = "zoe"
}
A, B 구조체를 선언하고 각각의 메모리 주소를 확인해봤을때
var structA = MyStruct()
var structB = structA
address(o1: &structA) // 0x10aa38950
address(o1: &structB) // 0x10aa38960
메모리 주소가 다른것을 확인할 수 있었다. 즉, 할당하는 순간 복사가 일어났다고 할 수 있다.
왜지 분명 수정할때 복사가 일어난다고 했는데..?
COW가 작동하고 있지 않네! 라고 생각할수도 있다
근데 이 결과가 옳은 결과이다. 왜냐하면 커스텀 타입에는 COW가 구현되어있지 않기 때문이다. 즉 직접 COW를 구현해줘야 한다는 의미이다.
기본 데이터 타입들(예: String, Array 등)은 Swift 표준 라이브러리에서 제공하며, Copy-on-Write 최적화가 구현되어 있다. 따라서 이들 타입의 인스턴스는 값이 변경될 때만 실제로 복사가 일어나게 된다.
그러나 사용자가 정의한 구조체에 대해서는, 이런 최적화를 수동으로 구현해야 한다. 만약 수동으로 CoW를 구현하지 않는다면, 구조체 인스턴스는 메모리에 할당될 때마다 복사된다.
Swift에서 제공하는 기본 데이터 타입들은 COW가 구현되어 있다고 했다.
따라서 컬렉션 타입 중 하나인 Array에서 복사가 최적화 되는지 먼저 살펴보자!
가설1. 값타입을 복사하기 전에는 메모리 주소가 같아야 한다
var numberA = [1, 2, 3, 4]
var numberB = numberA
address(o1: &numberA) // 0x6000024d9ea0
address(o1: &numberB) // 0x6000024d9ea0
주소를 확인해보니 정말 같은 메모리 주소를 공유하고 있는것을 확인할 수 있었다.
가설2. 복사후 값을 수정하면 메모리 주소가 달라야 한다
var numberA = [1, 2, 3, 4]
var numberB = numberA
numberB[0] = 3
address(o1: &numberA) // 0x6000009044a0
address(o1: &numberB) // 0x6000009046e0
0번째 값을 수정해주었더니, 그제서야 복사가 일어났고
-> 메모리 주소가 달라졌음을 확인할 수 있었다.
가설1,2가 모두 맞는것으로 보아 Array에서는 COW가 잘 작동한다고 할수있다.
즉, 배열을 복사하면 원본 리소스를 공유하고 있다가, 값의 수정이 일어나면 실제 복사가 이루어진다는 것을 알게되었다.
먼저 Set으로 실험해보면
가설1. 값타입을 복사하기 전에는 메모리 주소가 같아야 한다
var numberA: Set = [1, 2, 3, 4]
var numberB = numberA
address(o1: &numberA) // 0x106b70970
address(o1: &numberB) // 0x106b70978
결과: 0x106b70970
vs 0x106b70978
주소값이 다르다. 왜지? 분명 값을 수정하지 않았는데 메모리 주소가 다르게 나온다. COW가 되고있지 않다고 할 수 있다.
그렇다면 딕셔너리도?
var dictionaryA = ["0": 0]
var dictionaryB = dictionaryA
address(o1: &dictionaryA) // 0x108574990
address(o1: &dictionaryB) // 0x108574998
결과: 0x108574990
vs 0x108574998
딕셔너리 역시 값을 수정하기 전부터 두 인스턴스의 메모리 주소가 다르게 출력되었다.
왜그런걸까? COW는 Array에만 적용 되는 건가?
고민중에 스택오버플로우에서 글을 발견했다.
위 글을 해석해보면
컬렉션 타입은 결국 값 타입 이기 때문에 값이 전달되는 그 순간 즉시 새로운 메모리 주소가 할당될 가능성이 있다. 그들의 backing storage만 공유될 뿐이다.
라고 적혀있는데, 사실 이분들도 정확히 모르는 것 같다.
답변이 전부 "가능성이 있다" 로 적혀있기도 하고, 뭐가 정답인지 모르겠다.
결국 명쾌하게 답을 못내린채 실험을 종료했다..
아무리 공식문서에서 Set에 COW가 구현되어 있기 때문에 최적화된다고 하더라도 실제와 다르다는 점이 놀라웠다. COW로 인해서 메모리 주소를 공유할 수 있지만, 무조건 그렇게 동작하는건 아니라는 것만 알고 넘어가려고 한다.