- Struct로 정적 디스패치지만 프로토콜로 인해 다형성을 가지게 된다.
- drawables에 Point와 Line모두 들어가는 것이 가능하다.
- 하지만 Struct Line과 Struct Point는 V-Table 디스패치를 수행하는데 필요한 공통 상속 관계를 유지하지 않는다.
그렇다면 아떻게 배열을 도는 동안 올바른 메소드로 디스패치를 하는 것 일까?
Protocol Witness Table
- 이렇게 각자의 테이블을 형성하고 연결이 되어 있다.
- 어떻게 배열의 요소에서 테이블로 이동을 할까?
- Line은 4개의 프로퍼티를 가지고 있고
- Point는 2개의 프로 퍼티를 가지고 있다.
- 배열은 요소들을 고정된 크기로 균일하게 저장하려 한다.
- 어떻게 가능할까?
Existential Container
- swift가 Existential Container라는 저장소 레이아웃을 사용하기 때문이다.
- 3단어의 valueBuffer가 존재
- Point처럼 2개의 공간만 필요한 작은 유형은 valueBuffer에 적합하다.
- 그러면 line같이 4개 이상인 것은 어떻게 할까?
- 힙에 메모리를 할당하고 값을 저장한뒤 해당 메모리에 대한 포인터를 existential Container에 저장한다.
- 이러면 클래스 힘에 저장하고 포인터 저장하는 거랑 다른게 뭘까?
- 이렇게 다르게 저장하면 차이점을 둬야 한다.
- Value Witness Table로 차이점을 둔다.
Value Witness Table
- porotocol type 지역 변수의 lifeTime이 시작될 때 swift는 해당 테이블 내부에서 allocate함수를 호출한다.
- 힙에 메모리를 할당하고 포인터를 valuBuffer에 저장한다.
- 지역 변수를 초기화하는 소스 값을 Existential container로 복사를 한다.
- Line의 존재를 확인 value witness table의 copy항목이 힙에 할당된 valueBuffer에 할당한다.
swift calls the destuct entry in the value witness table, which will decrement any reference counts for values that might be contained in our type
- desturct 항목을 호출하여 타입의 인스턴스가 더 이상 필요하지 않을 때 참조 카운트를 감소시킨다.
- 근데 struct인데 참조 카운트가 있나?
- 내부에서 사용하는 참조 카운트를 관리하는 것 같다.
- 근데 struct인데 참조 카운트가 있나?
- deallocate 를 호출하여 힙에 할당된 메모리가 할당 해제한다.
- existential container에는 value witness table에 대한 참조가 있다.
- protocol witness table이 existential container로 참조가 된다.
요약?
- swift는 Line같이 큰 타입은 Heap에 메모리를 할당하고 해당 메모리에 대한 포인터를 Existential Containe에 저장한다.
- value Witness Table로 stored property의 lifeTime을 관리한다.
- protocol Witness table로 protocol method를 관리한다.
이렇게 이해하는게 맞는지는 모르겠다.
위의 과정을 코드로 예시를 들어보자면
- drawACopy라는 함수가 실행이 되면 val이라는 인수를 수신하여 함수에 전달한다.
- 생성된 코드에서 Swift는 힙에 existential container를 할당한다.
- 이 부분은 강연자는 heap에 할당이 된다고 하는데 그림에는 Stack에 할당이 된다.
- 그리고 existential container에서 vwt와 pwt를 읽고 local를 초기화 한다.
- Existential Container는 stack에 할당
- VWT, PWT는 힙에 할당 그리고 이것들을 가리키는 포인터들이 Existential Container에 저장되어 초기화가 된다.
그리고 buffer를 할당하고 값을 복사하기 위해 copy가 호출이 된다.
그러나 drawACopy가 Line을 전달했다면 buffer를 할당하고 거기에 값을 복사할 것이다.
- heap에 Line을 할당하고 전달
- 그리고 나서 draw 메서드를 실행하고 Swift는 existential container에서 PWT를 조회한다.
- 그리고 해당 draw메서드를 조회하고 구현부로 넘어가게 된다.
- 하지만 그 전에 draw메서드는 해당 사용되는 내부 값들을 원한다.
- 그렇기 때문에 Point처럼 작은 값이면 existential container의 시작 부분을 가리킬 것이고
- Line처럼 큰 값이면 heap에 할당된 메모리를 가리킬것
- 함수를 호출하고 나서는 이제 범위를 벗어나기 때문에 참조가 있는 경우에는 참조 카운트를 감소 시키고 버퍼가 할당된 경우 버퍼 할당을 해제 한다.
- 그리고 함수 실행이 완료 되면 스택이 제거되어 스택에 생성된existential container가 제거 된다.
요약
- 함수를 실행한다.
- 함수의 인수가 들어간다.
- 해당 인수에 대한 Existential Container가 생성되고 스택에 할당된다.
- 해당 인수에 대한 타입 메타데이터가 힙에 할당된다. Existential Container는 타입 메타데이터 포인터, Value Witness Table (VWT) 포인터, Protocol Witness Table (PWT) 포인터를 포함하여 초기화된다.
- 해당 인수의 프로퍼티 값이 작으면 그대로 Existential Container의 valueBuffer에 할당된다.
- 값이 크다면 힙에 값을 할당하고, 이를 가리키는 포인터가 Existential Container의 valueBuffer에 할당된다.
- 내부 코드가 실행된다. 내부 코드가 인수의 값을 필요로 하면 Existential Container를 통해 접근한다.
• 필요 시 VWT를 사용하여 메모리 접근 및 관리를 수행한다.
- 내부 코드 실행이 끝나면 참조가 있는 경우 참조 카운트를 감소시키고, 힙에 할당된 메모리는 해제한다.
- 함수 실행이 완전히 완료되면 스택에 생성된 Existential Container가 제거된다.
이러한 작업들은 struct와 같은 값 유형이 프로토콜과 결합하여 동적으로 동작하거나, 동적 다형성을 얻게 한다.
- 이러한 역동성을 가지는데 사용하는 비용은 class의 V-Table를 거치고 참조를 계산하는 추가 오버헤드 보다 작다.
Protocol Type Stored Properties
다음과 같이 Struct 내부에 protocol를 채택하는 프로퍼티들이 있다.
이런 경우에는 어떻게 될까?
- 2개의 Existential Container를 생성하고 pair Strcut에 저장한다.
- 위에서 한것과 동일하게 각자의 크기에 따라서 buffer에 저장이 된다.
- 2개를 힙에 할당하는 경우를 알아보자
- 스택에 2개의 Existential Container가 할당이 되고 4개의 Line이 힙에 할당된다.
- 하지만 힙 할당은 비싼 비용을 사용한다고 한다.
- 그렇다면 어떻게 해야할까?
- Line이 Class라면 heap에 1번 할당이되고 그것을 참조하는 것이 first와 second에 들어 가기 때문에 heap에는 1개만 할당이 이루어 진다.
- 그러면 참조가 복사가 되기 때문에 힙 할당에 관한 비용이 아닌 참조의 복사분에 대한 비용만 지불하면 된다.
- 하지만 이렇게 하면 class의 특성상 우리가 프로퍼티를 수정하는 경우 참조 타입이라서 값이 변하게 된다.
- 이러한 것을 방지하는 방법은 무엇이 있을까?
- 우리가 원하는 모습은 이런 모습이다.
COW ( Copy on Write )
- Line 구조체의 프로퍼티 값들을 LineStorage가 가지게 되고
- LineStorage를 Line의 프로퍼티로 가지게 된다.
- 값을 읽고 싶을 때 해당 저장소의 내부의 값을 읽으면된다.
- 값을 수정하거나 변경하려면 레퍼런스 카운트를 확인한다.
- 카운트가 1보다 크다면 LineStorage의 복사본을 만들고 이를 변경한다.
- LineStorage를 다른곳에서 참조를 하고 있다면 복사를 해서 새롭게 만드는 것 같다.
Copy Using Indirect Storage
- 동일한 LineStorage를 참조한다.
- 이것은 힙 할당보다 저렴하다고 한다.
- 작은 크기로 valueBuffer에 들어가는 것은 힙에 할당을 안해도 된다,
- 구조체가 참조를 당하지 않으면 RC가 없다
- VWT와 PWT로 동적 디스패치의 모든 기능을 얻을 수 있다.
- 큰 값은 초기화하거나 할당할 때마다 힙 할당을 발생시킨다.
- 큰 값 구조체에 참조가 포함된 경우 RC를 계산한다.
- 하지만 COW나 indirect Storage를 통해 비용이 많이 드는 힙 할당을 교환하는데 사용이 가능하다.
- 힙 할당 비용이 줄고 RC계산 비용이 올라간다.
- 프르토콜 유형은 동적인 형태의 다형성을 제공
- value type을 사용 가능
- 배열과 같은 곳에 프로토콜 타입의 값을 저장 가능 하다.
- VWT와 existential container로 가능해진다.
- 큰 값을 저장하면 힙 할당의 비용이 커지는데 이것은 cow와 indirect storage를 통해 해결이 가능하다.
Generic
- foo의 매개변수에 point가 들어가면 point의 타입인 Point를 바인딩한다.
- 해당 draw가 point의 draw인지 line의 draw인지 알기 위해서 VWT와 PWT를 사용한다.
- 그러나 호출 컨텍스트당 하나의 유형이 있기 때문에 Swift는 여기서 existential container를 사용하지 않는다. ?? 왜지….
- 대신 추가적인 인수로 VWT와 PWT를 전달한다.
- 함수를 실행하면 매개변수에 대한 local 변수를 만들고, swift의 value witness table를 사용하여 필요한 버퍼를 힙에 할당하고 복사를 한다.
- 마찬가지로 draw메서드를 실행할 때는 PWT를 참고해서 메서드로 점프한다.
- Existential Container가 없는데 로컬 매개변수에 필요한 메모리를 할당할까?
- 스택에 valueBuffer를 할당한다.
- line과 같이 큰 값은 다시 힙에 저장하고 local exitential container에 포인터를 저장한다. ?????????
- exitential container가 없다고 했는데 여기에 저장한다고 한다.
- 이렇게 2개의 draw함수를 사용하는 코드가 있다
- swift는 point의 함수를 inline해서 최적화를 한다.
- 이렇게 specialization이 된다.
- 언제 specialization이 일어날까?
- swift가 specialization을 하기 위해서는 call site로 가서 타입을 추론 할 수 있어야 한다.
- 두 파일로 코드를 나누었다. 만약 별도로 컴파일을 하면 point의 정의를 더 이상 사용할 수 없다.
- 그러나 전체 모듈 최적화를 사용하면 컴파일러가 2개의 파일을 하나의 단위로 컴파일을 하기 때문에 최적화가 가능하다.
- 기존의 코드는 이렇게 2개의 Drawble를 가진다.
- 근데 이것을 제네릭을 사용하면 강제 1개의 타입만 사용하게 강제 시킬 수 있다.
이렇게 하면 추가적인 힙 할당이 발생하지 않는다.
- struct는 힙 할당이 필요하지 않고
- 레퍼런스가 포함이 안되어 있다면 참조 계산을 할 필요가 없고
- static method dispatch가 가능하기 때문에 성능이 좋다.
- 클래스는 다 반대
- generic을 사용하지 않는 작은 값은 PWT를 이용한 dynamic 디스패치가 일어난다.
- 큰값은 heap할당이 필요해지고 heap할당으로 인한 참조 발생으로 참조 카운트 계산을 하게 된다.
- 런타임 요구사항에 맞는 추상화를 해야한다.
- value타입 같은 값 유형을 사용하면 최적화가 가능하다.
- 클래스 같은 경우 final 키워드 같이 상속을 막아서 동적 디스패치를 정적 디스패치로 변경하는 등의 방법으로 비용 줄이는 것이 가능하다.
- Generic을 사용하여 빠른 코드를 얻을 수 있다. 하지만 코드에 대한 구현을 공유 할 수 있다.
- protocol을 사용하여 값 타입과 사용하면 다형성을 얻고 클래스보다 비교적 빠른 코드를 얻을 수 있지만 참조가 아닌 값 타입에 머문다.
- 프로토콜과 제네릭 같은 경우 큰 값을 사용할 때 나오는 힙 할당의 문제는 COW와 indirect로 문제를 해결하는 것이 가능하다.
'swift' 카테고리의 다른 글
WWDC 16 Understanding Swift Performance (1/2) (0) | 2024.06.19 |
---|---|
Closure 클로저 (0) | 2024.05.30 |
Initialization 초기화 (0) | 2024.05.25 |