Reference
value type도 다형성을 지원할 방법이 없을까? 답은 POP에 있다
위와 같이 Drawable
타입의 Array에 Point
/Line
인스턴스를 저장할 수 있다. 이는 class 상속처럼 다형성을 지원하는 것이지만 한 가지 차이점이 있습니다
Point
와 Line
은 Drawable
에 대한 다형성은 지원하면서, struct이기에 V-Table로의 주소값은 가지고 있지 않습니다. 또한, V-Table은 공통의 부모를 가지는 상속관계를 다루기 위해 만들어졌으므로 Protocol+struct 조합을 다루기 위해선 다른 메커니즘이 필요합니다
프로토콜은 V-Table 대신 PWT라는 테이블기반 메커니즘을 사용합니다. PWT는 해당 프로토콜을 채택한 타입당 테이블을 하나씩 가집니다. 테이블 내 entry들에는 메서드 구현체로의 주소값이 들어있습니다
아까의 배열로 돌아와서, 아직 두가지 문제가 남았습니다. (1) 하나는 class와 달리 struct는 테이블로의 주소값을 별도로 저장하고 있지 않기에 어떻게 테이블로 향할 수 있냐는 점입니다. (2) 다른 하나는 일정한 메모리 크기의 요소만 저장할 수 있는 Array가 어떻게 서로 다른 타입의 struct 인스턴스를 저장할 수 있느냐하는 점입니다 (class는 reference semantic인 이유로 어느 타입이든 실제 인스턴스 크기와 무관하게 Array에 저장할 reference의 메모리 크기는 uniform하기에 문제가 되지 않았었습니다)
(2)번 "Array가 서로 다른 크기의 인스턴스를 저장할 방법"을 해결하기 위해, 일정한 크기를 가지면서 프로토콜 타입을 저장하는 Existential Container
라는 특별한 5 word짜리 storage layout이 도입되었습니다
첫 3 word는 valueBuffer
를 위해 예약되어 있어 여기에 예제의 Point
처럼 3 word 이하로 필요한 값타입을 직접 저장할 수 있습니다. 여기서 Line
처럼 3 word를 초과하면 Heap을 할당하여 거기에 저장하고 valueBuffer에는 Heap으로의 주소값을 대신 저장합니다. 이를 위해 3 word를 확인하고 그 결과에 따라 할당하는 행위를 관리하기 위한 메커니즘이 또 등장합니다
프로토콜 타입 변수의 lifetime을 관리하기 위해 VWT라는 메커니즘이 도입되었습니다 (VWT는 타입마다 하나씩 가지게 됩니다).
VWT동작
1. allocate : 프로토콜 타입에 변수가 할당되면, 3 word check 후 필요하다면 Heap 공간 확보
2. copy : valueBuffer 혹은 할당받은 Heap으로 소스 인스턴스 복사
3. destruct : 사용이 끝나면, 복사했던 정보 삭제
4. deallocate : destruct 이후, 할당받았던 Heap 공간 반환
아직 (1)번 문제 "어떻게 테이블로 향할 수 있는가?"를 알아내지 못했습니다. 짐작가듯이 이 정보는 Existential Container에 저장됩니다. 처음 소개할 때 Existential Container가 5 word라고 말했었고, 3 word는 valueBuffer로 사용된다고 했습니다. 나머지 2 word에 각각 PWT
와 VWT
의 주소가 저장됩니다
Point()
인스턴스로 Existential Container를 만들어 val
에 저장
drawACopay(val)
가 호출되면 파라미터 local
지역변수 초기화. 우선 val
의 pwt
/vwt
를 복사
복사해온 vwt
의 allocate
/copy
를 수행하여 value도 확보
draw()
를 수행하기 위해, pwt
로 가서 테이블 내 draw에 해당하는 fixed offset에 있는 구현체로 이동
draw()
구현체에 자기자신(local
인스턴스 value)을 넘겨야하는데, vwt
함수 중 projectBuffer
란 함수를 이용. 이 함수는 인스턴스 value의 시작점을 return하므로 3word 이하면 Existential Container의 시작점, 3word 초과면 할당했던 Heap의 시작점을 return한다
drawACopy()
종료시점이 되면 vwt
의 destruct
를 호출하여 local
의 value를 해제. 이후, 함수가 종료되며 Stack에 있던 local
Existential Container 자체도 해제됨
이 모든건 value 타입에게 다형성을 지원하기 위함이다. class 상속에 비해 reference counting 오버헤드가 적다는 점이 유리하다
프로토콜 타입 stored 프로퍼티는 Existential Container로 저장된다. 위 예제에서 Pair
타입의 initializer에서 first
/second
프로퍼티에 대해 일어나는 일은 이전에 알아본 "함수 argument로 프로토콜 타입 value가 전달되는 과정"과 동일하게 이루어진다
또한, 이런 메커니즘으로 인해 3word 이내여서 valueBuffer에 직접 저장하던 second = Point()
를 위와 같이 Heap을 요구하는 second = Line()
으로 변경하는 것도 얼마든지 가능하다. (다만 예제에선 둘다 Heap으로 할당되다보니 비용은 큰 편이다)
위 코드를 살펴보자. 이는 복사를 하며 2개의 추가 Heap 할당이 필요하여 총 4개의 Heap 할당이 필요한 코드이다. 뭔가 방법이 없을까?
sharing을 허용하는 경우에 한해서 Line
을 class로 변경하면 추가 Heap 할당을 막을 수 있긴 하다. reference semantic 특성상 원본 인스턴스에 대한 참조값으로 1 word만을 사용한다. 따라서, valueBuffer의 3word check를 항상 통과하므로 별도의 Heap 할당없이 동일한 class 인스턴스에 대한 참조값 복사만 이루어진다
하지만 결국 sharing을 허용하는 꼴이므로 근본적인 해결책이 아니다. 개선방법이 없을까?
sharing을 하다가 write가 들어오면 copy를 하는 메커니즘인 copy on write
가 한 방법이 될 수 있다. 위 코드는 reference count가 unique하지 않다면 sharing을 막기 위해 새로운 LineStorage
인스턴스를 생성(copy)하도록 하는 예제이다
Indirect Storage를 만들어 copy on write를 적용할 경우 위와 같이 메모리와 성능비용을 아낄 여지가 있다
Small Value
- Heap 할당이 없으므로 value 타입의 장점을 온전히 가질 수 있다
- value가 reference type을 가지는 경우를 제외하면, reference counting이 없다
- 그럼에도 불구하고 다형성을 온전히 누릴 수 있다
Large Value
- Heap 할당 비용이 발생한다
- value가 reference type을 가지는 경우, reference counting이 발생한다? (이 부분은 small이든 large든 동일한 것 같은데;; 무슨 차이일까.. Indirect Storage처럼 과도한 Heap 할당을 막기위해 잠재적으로 reference counting을 사용해야 할 수도 있다?)