이전 포스팅 [Swift] Structures and Classes의 deep copy (깊은 복사)를 다루면서,
'컬렉션 타입의 구조체와 String 타입에는 COW (copy-on-write)가 내장 구현되어 있어 값 복사가 일어나도 동일한 메모리 주소를 갖고 있다.' 라고 했다.
Collection Type에서는 COW가 기본적으로 구현이 되어있는데, Array
를 통해서 COW를 확인해 보았다.
들어가기 앞서, Swift의 Array가 어떻게 메모리에 저장되는지 알아볼 필요가 있다.
var intArr: Array<Int> = [0, 1, 2]
Array 구조체 타입의 인스턴스에 배열 리터럴 값을 할당하면 ExpressibleByArrayLiteral 프로토콜의 'init(arrayLiteral:)' 메서드에 의해 초기화가 된다.
init(arrayLiteral elements: Element...) 의 argument label인 arrayLiteral
은 'Element...' 타입으로 이니셜라이저가 호출될 때, 전달된 인수들을 배열로 묶어서 받을 수 있다.
💡
...
은 '가변 매개변수 (varadic paramters)' 로써, 0개 이상의 특정 타입의 값을 허용한다.
여기서 중요한 건!
가변 인수로 전달되는 배열 요소들은 Stack 영역이 아닌 'Heap 영역에 저장된다'는 것이다.
이는 인수가 '가변적' 이며 컴파일 타임에 '정확한 크기를 알 수 없기 때문에',
구조체의 인스턴스는 실질적으로 Stack에 저장되지만, call by value에 의해 새롭게 생성된 배열요소와 메타데이터는 Heap에 저장된다.
좀 더 자세하게 Array 인스턴스가 어떻게 생성되고 메모리 공간에 어떻게 할당되는지 Array 구현부를 파헤쳐 보았다.
copy-on-write (이하 COW)는 데이터를 복사할 때, 메모리를 효율적으로 관리하기 위해 사용하는 기법으로 '데이터를 쓰기 시 복사' 하는 방법이다.
즉, 데이터에 '변경이 일어나기 전'에는 '데이터 복사가 안 된다!'
var arr: Array<Int> = [1, 2, 3]
let arrPtr: UnsafeRawPointer = UnsafeRawPointer(arr)
var newArr: Array<Int> = arr
let newArrPtr: UnsafePointer = UnsafePointer(newArr)
Array 타입의 구조체 인스턴스 변수인 arr
와 newArr
,
각 변수의 포인터 상수 arrPtr
과 newArrPtr
을 선언 및 초기화 했을 때,
func checkAddress(of object: UnsafeRawPointer) -> String {
let address: Int = Int(bitPattern: object)
return String(format: "%p", address)
}
// MARK: - `arr` & `arrPtr`
print("address of `arr`: \(checkAddress(of: &arr))")
arr.withUnsafeBufferPointer {
guard let baseAddress: UnsafePointer<Int> = $0.baseAddress else { return }
print("address of `arr`: \(baseAddress)")
}
print("value of `arrPtr`: \(arrPtr) \n")
// MARK: - `newArr` & `newArrPtr`
print("address of `newArr`: \(checkAddress(of: &newArr))")
newArr.withUnsafeBufferPointer {
guard let baseAddress: UnsafePointer<Int> = $0.baseAddress else { return }
print("address of `newArr`: \(baseAddress)")
}
print("value of `newArrPtr`: \(newArrPtr)")
메모리 주소를 확인할 수 있는 checkAddress(of:)
,
배열 요소에 대한 포인터를 얻을 수 있는 함수 withUnsafeBufferPointer(_:)
를 통해 메모리 주소값을 출력하였고,
각 인스턴스 변수 대한 포인터 상수의 값을 출력하면 동일한 주소값이 출력된다.
Instruments를 통해 힙 영역 저장된 ContiguousArrayBuffer<Element> 구조체의 storage
프로퍼티(class ContiguousArrayStorageBase)에 2개의 참조타입 인스턴스가 할당되었다.
즉, 값 변경이 일어나기 전까지 두 배열은 같은 리소스를 공유한다.
💡 리소스 공유를 '참조' 의 개념으로 생각해도 무방하다.
var newArr: Array<Int> = arr // No copy will occur here.
이제 원본 데이터 arr
또는 사본 데이터 newArr
에 수정이 발생하면
newArr.append(3)
print("address of `arr`: \(checkAddress(of: &arr))")
arr.withUnsafeBufferPointer {
guard let baseAddress: UnsafePointer<Int> = $0.baseAddress else { return }
print("address of `arr`: \(baseAddress)")
}
print("value of `arrPtr`: \(arrPtr) \n")
print("address of `newArr`: \(checkAddress(of: &newArr))")
newArr.withUnsafeBufferPointer {
guard let baseAddress: UnsafePointer<Int> = $0.baseAddress else { return }
print("address of `newArr`: \(baseAddress)")
}
print("value of `newArrPtr`: \(newArrPtr) \n")
비로소 그 때, 진짜 값 복사가 이루어지고 서로 다른 주소값을 갖게 된다.
(재실행을 했기에 이전 메모리 주소와는 다른값이지만, 마지막 출력 3개를 통해 다른 메모리 주소를 할당 받았다는걸 알 수 있다.)
💡 말 그대로 '쓰기 시 복사' 가 이루어짐을 확인할 수 있다.