본문 바로가기

Swift/Deep dive!!

[Swift] CoW(Copy-On-Write) 기법과 String은 값타입일까? 참조타입일까?

728x90

 

안녕하세요. 오늘은 Swift에서 값타입과 참조타입이 어떻게 효율적으로 관리되는지 알 수 있는 CoW기법에 대해 소개하려고 합니다. 그리고 JK iOS 마스터님이 실험한 내용을 살짝 정리해보려고 합니다.

 

 

근데 struct는 값에 의한 복사가 일어난다고 하는데 struct는 값 타입일까요? 참조타입 일까요.?

String은 값타입인가 참조타입인가?

 

String의 정의 입니다. 흔히 struct하면 떠오르는 것은 값 타입!! 입니다. iOS Master JK님의 타입별 메모리 분석 실험을 봤는데 어.. 이거 예전에 이 글 본 것 같았는데.. 오호. Struct로 정의된 String은 값 타입인지에 대해 실험하는 글인데요. 

 

위 링크의 포스트 중 String 파트에 대해서 정리와 설명을 하려고 합니다.

String파트에 dump(with:)랑 dump<T>(variable: inout T) 두개의 메서드가 있습니다. 아.. 그전에 Swift의 클래스는 참조타입이지만 포인터를 찾아볼 수 없는데,, 사실 Swift도 unsafePointer 등 포인터를 다룰 수 있는 함수들이 몇 있습니다(관련 링크).

 

 JK님의 타입별 메모리 분석 글을 분석하기 위해 사전 지식으로 이 링크에서 나오는 함수 중 두 개의 함수에 대해 간략 설명을 하겠습니다.

 

dump<T>(variable: inout T)

'inout' 키워드를 사용하는데 보통 inout은 매개변수에 값 타입의 값을 받을 때 복사하지 않고 메모리를 참조하기 위해 사용됩니다. 이 함수 안에 있는 withUnsafePointer(to:body:)는 to에 변수나 인스턴스를 지정하면(여기서는 T타입이네요 == String, Int ....올 수 있다!!) 클로저형식으로 해당 인스턴스나 변수의 포인터를 전달받을 body에서 받을 수 있습니다. 주로 값 타입일때 참조하기 위해 사용됩니다.

 

dump(with:)

unsafeRawPointer로 접근을 했을 때의 포인터! 포인터로 메모리를 참조합니다. 그래서 struct같은 값 타입은 안됩니다. 포인터가 있거나 참조를 할 수 있는 경우에만 dump(with:)함수를 사용할 수있다는 것입니다.

 

정리하자면 dump(with:)함수의 매개변수 타입인 unsafeRawPointer는 참조 타입만 값을 넣을 수 있다. 그런데 JK님의 타입별 메모리 분석 실험에서 Int 타입의 프로퍼티를 dump(with:) 의 매개변수로 넣었을 때는 안됬지만 같은 struct로 설계된 String의 경우.. 출력이된다. == 포인터로 접근이 가능하다?! (struct)인 String이?!

 

 

여기서 let str1 = "abcd" 변수를 생성했을 때 ..ff418은 str1이 할당된 메모리의 주소, ca510은 힙영역 특정 주소입니다.

str1변수에 값을 변경 했을 때 여젼히 스택은 주소값이 변하지 않습니다. 그런데 포인터 주소는 바뀌었습니다. 근데 여기서 

let str2 = str1로 새로운 String을 생성해 할당했을 때, str1 메모리 영역인 ...ff418과 str2의 메모리 영역 ...ff370은 다르지만!!!(다른 스택 영역에 저장되어 있지만) 그 주소 값 출력 아래 주소값 출력은 ...ca1f0(포인터 주소값) 힙 영역은 같습니다.

 

이때 CoW(Copy)의 개념을 알 수 있는 것 같습니다.(왜냐면 String의 경우 struct이지만 unsafeRawPointer를 사용했을 때 포인터 주소가 찍혔고 (마치 참조 타입처럼) 위의 경우에서 복사를 했을 때 포인터 주소는 변하지 않았기 때문입니다. m1 맥에서도 실험을 하셨는데 안될때도 있지만 된 경우도 링크의 포스트에 나와았습니다. ( 저도 되네요 )

 

그래서 CoW가 뭐냐!!

Swift CoW(Copy-on-write)

Copy-on-Write란?

 

타입(컬랙션 타입의 Dictionary, Set, Array등)의 인스턴스가 복사를 할 때, 실제 원본이나 복사본( str2 = str1)이 수정되기 전까지는 스택 주소는 서로 다른 객체이기에 서로 다른 주소에 할당되지만!!!!!!!!!!!!! 참조를 통해 같은 데이터를 사용한다는 개념입니다. 물론 Int는 그렇지 않지만요.

 

var note = ["Struct는 CoW기법을 쓴다", "신기하네,,"]
var newNote: [String] = note // 값 복사?!

 

왜 그럴까요? note 배열을 생성하고, newNote 배열을 만들어 note의 값을 대입했을 때(복사. 이유: 배열은 값타입) note의 값을 공유해 newNote도 사용하게 됩니다. 하지만 값이 변경되는 순간 복사를 하게 됩니다. 이 개념이 CoW입니다.

 

// JK님의 dump(with:)
@inlinable func dump(with: UnsafeRawPointer) -> String {
  let address = Int(bitPattern: with)
  return String(format:"%018p", address)
}

print("18: --------CoW기법에 의해 힙 영역 참조--------")
print("19: note", dump(with: note))
print("20: newNote", dump(with: newNote))

newNote[0] = "실제 복사"
print()
print("24: --------newNote 수정--------")
print("25: note", dump(with: note))
print("26: newNote", dump(with: newNote))

 

실제로 이렇게 unsafeRawPointer에 대입하게 된다면 포인터 주소를 알 수 있는데(struct임에도 불구하고). 포인터 주소를 통해 두 프로퍼티가 한 주소를 가리키는 지 여부를 알 수 있습니다.

 

 

newNote[0]의 값을  변경하는 순간 newNote의 포인터 주소는 서로 다른 주소를 갖게 됩니다.

 

728x90