Swift

Swift의 메서드 실행 방법 (3) | 프로토콜 채택한 구조체 Existential Container / ValueBuffer / VWT / PWT

하이D:) 2024. 1. 29. 09:52

Swift의 메서드 실행 방법에 대한 글에서 우리는 Swift가 메서드를 실행시키는 세 가지 방법과 테이블 기반 메커니즘인 Virtual Table과 Witness Table에 대해 알아보았다.

 

 

Swift의 메서드 실행 방법 (1) | Direct(Static) Dispatch/ Table(Dynamic) Dispatch/ Message Dispatch

Swift의 메서드 실행 방법 (2) | table 기반 메커니즘 Virtual Table과 Witness Table

 

 

저번 글의 마지막에서 프로토콜을 채택한 구조체가 있을 때 이 인스턴스에서 메서드를 찾아서 실행하기 위해서는 Existential Container를 거쳐야 한다고 설명했다. 

 

이번 글에서는 프로토콜을 채택한 구조체가 저장되는 방법인 Existential Container와 메서드를 실행시키는 방법에 대해 알아볼 것이다. 클래스 인스턴스, 단순한 구조체 인스턴스가 저장되는 방법과 사뭇 달라서 처음엔 이해하기 어려웠지만 최대한 이해한 대로 잘 정리해 보겠다..!!

 

 

 

Existential Container

Existential Container가 무엇이고 왜 프로토콜을 채택한 구조체들에 대해서 메서드를 찾아 실행하기 위해 필요하다는 건지 알아보자.

 

Existential Container는 프로토콜을 채택한 구조체의 값을 저장하기 위한 방법이다.(즉, 프로토콜 타입의 “값”들을 저장하기 위해 필요한 것!) WWDC에서 썼던 표현을 인용하면 “Boxing values of protocol types(프로토콜 타입의 값을 박싱한다)”이다.

 

 

Existential Container는 크기가 총 5 words 인 컨테이너이다. 그럼 word에 대해 궁금할 것이다..ㅎㅎ word라는 건 간단히 “64비트 기기에서 64비트를 나타내는 단위”라고 이해하면 쉽다. 일단 Existential Container의 크기가 총 5 words라는 건 어떤 컴퓨터 내부 단위로 5칸이 필요한 것이라고 생각하고 넘어가 보자.

 

그리고 이 Existential Container의 내부는 어떻게 구성되어 있는지 살펴보면 3칸의 value buffer, 1칸의 value witness table 포인터, 1칸의 protocol witness table 포인터로 구성되어 있어서 총 5칸(5 words)인 것이다.


그림으로 보면 대충 이해가 갈 것이다.

 

 

예제 코드는 WWDC에서도 나온 이전 글과 동일한 프로토콜과 구조체 코드를 바탕으로 설명하겠다.

 

protocol Drawable {
  func draw()
}

struct Point: Drawable {
    let x, y: Double
    
    func draw() {
      print("Point 구조체의 메서드 실행 => \(x), \(y)")
    }
}

struct Line: Drawable {
    let x1, y1: Double
    let x2, y2: Double
    
    func draw() {
      print("Line 구조체의 메서드 실행 => \(x1), \(y1) - \(x2), \(y2)")
    }
}

var drawables : [Drawable]

for d in drawables {
	d.draw()
}

 

 

 

Value Buffer

Existential Container가 차지하는 5 words 중에서 3 words를 차지하는 value buffer는 무엇일까?

buffer는 구조체의 “값”을 저장하기 위해 필요하다. 근데 이 값이 크기가 얼마나 하는가 에 따라 저장방법이 다르다.

 

3 word 이하일 때

- Existential Container의 buffer영역에 inline으로 저장시킨다.

3 word 초과할 때 

- VWT의 allocate 메서드를 사용해서 힙 영역에 메모리를 할당한다.


위의 코드에서 Point는 프로퍼티 2개, Line는 프로퍼티 4개 인 것을 볼 수 있는데,

이 상황에서 Point의 저장속성 값은 inline으로 buffer에 저장될 수 있는 것이고, 3 words를 초과하는 Line의 저장속성은 힙 영역에 따로 저장되어야 한다. 그리고 이 경우  힙 영역을 가리키는 포인터를 buffer에 위치시킨다.
힙 영역에 값이 저장되더라도 값이 사용될 때 이 값을 찾아갈 수 있어야 하기 때문이다.

But, value buffer 크기를 초과하든 초과하지 않든 구조체이기 때문에 Existential Container는 두 경우 모두 Stack에 저장된다.

 

 

 

 

VWT에 대한 포인터(Reference)

((왜 reference인가? 실제 VWT는 다른 곳에 위치 (Existential Container는 그곳을 가리키고 있을 뿐))

VWT 인스턴스 lifecycle 조작을 위한 것이다. 이렇게 이야기하면 이해하기 어렵고

우리가 프로토콜을 채택한 구조체를 인스턴스화할 때부터 ~ 인스턴스를 더 이상 쓰지 않아 메모리에서 해제하기 위한 순간까지를 관리하는 함수들이 있다고 생각하면 될 것 같다.

VWT는 실제 어떤 함수를 가지고 있는가?
인스턴스가 생성되고 메모리 해제되기까지의 lifecycle 순서로 VWT 내부에 존재하는 함수들을 설명하자면,

 

 

 

allocate : 인스턴스 값들의 메모리를 할당

  • 인스턴스의 프로퍼티 값들이 buffer의 용량에 맞으면 → 그냥 그 buffer에 메모리를 할당
  • 인스턴스의 프로퍼티 값들이 buffer의 용량을 초과할 때 (3 words 초과) → Heap에 메모리 할당하고 포인터만 buffer가 가지게 된다.

buffer용량을 초과하는 Line의 예시

 

 

 

copy : 실제로 값을 할당하는 단계

  • 인스턴스의 프로퍼티 값들이 buffer의 용량에 맞으면 → Existence Container 내부의 buffer에 값을 복사
  • 인스턴스의 프로퍼티 값들이 buffer의 용량을 초과할 때 (3words 초과) → Heap 영역에 값 복사

buffer용량을 초과하는 Line의 예시

 

 

 

 

destruct

  • 이 인스턴스가 더 이상 쓰이는 곳이 없을 때(할당된 변수를 더 이상 쓰지 않을 때 ) 호출 
  • → 값에 대한 레퍼런스 카운트(RC)를 감소시킨다.

사실 세션에서 말하는 “값(value)”가 정확히 어떤 것이고, 어떤 값의 RC를 감소시킨다는 것인지 완벽하게 이해하진 못했는데
아래 그림과 같이 프로토콜을 사용하고 있는 구조체에 대해 레퍼런스 카운트롤 세고 있기 때문에 → 사용이 종료되면 destruct 함수를 사용하여 RC 감소시키는 것인가? 싶기도 하다.

 

 

 

 

deallocate

RC도 감소하고 진짜 사용이 끝나면 deallocate 함수를 호출하고 용량이 커서 Heap에 값을 저장했던 Line의 경우에는 Heap의 메모리도 해제를 하게 된다.

 

 

우리는 swift가 어떻게 같은 프로토콜을 가지는 구조체의 다른 value들을 관리하는지 알아보았다.

 

 

 

PWT에 대한 포인터(Reference)

(왜 reference인가? 실제 PWT는 다른 곳에 위치 (Existential Container는 그곳을 가리키고 있을 뿐))

 

우리가 궁금했던 건 어떤 strct의 draw()를 실행시켜야 하는지였다. draw는 프로토콜에 요구사항으로 있던 메서드였기 때문에 내부적으로 PWT를 조회하게 되고 PWT에서 해당하는 draw 함수를 찾아서 실행하면 된다.

 

이게 바로 Protocol Witness Table의 쓰임새이다!

 

 

 

VWT에서 저장 프로퍼티 관리하고 PWT에서 메서드 관리한다고 볼 수 있겠다.

 

 

자.. VWT에 대한 포인터와 PWT에 대한 포인터는 각각 Existential Container의 VWT와 PWT에 저장되어 있는다고 했다. 그럼 실제 VWT와 PWT는 어디에 저장되어 있는 걸까?? 자료에서 보면 Heap영역인 것 같다…?

이미지 자료 기반으로 유추한 것일뿐 정확하진 않다!

 


 

프로토콜 타입의 “값”들을 저장하기위해, 그리고 실행한 메서드가 어떤 인스턴스의 메서드인지 찾아 실행하기 위해 필요한 Existential Container에 대해 알아보았다. 지금까지 설명한 내용을 그림으로 아주 간단하게만 나타낸다면 이렇게 될 것 같다.

 

 

다음 글부터는 Swift의 Performance에 대해서 글을 써볼 예정인데,

swift에서 성능을 결정하는 대표적 세 가지 요소를 알아보고 함께 이번 글에서 나온 것처럼 struct가 프로토콜을 채택했을 때 struct의 성능은 어떻게 변화하고 제네릭함수를 통해 어떻게 성능을 최적화할 수 있는지에 대한 내용을 정리해보려 한다!

 

이번 글에서 나온 내용과 이어지는 부분이 있기 때문에 함께 읽으면 swift의 성능과 구조체에 대해서 더 광범위하게 이해할 수 있을 것이다 :)