앞선 포스팅에서

Polymorphic(다형성)을 사용하기 위해선, Class를 사용해야 했다. 

 

하지만, Class는 아래와 같은 이유로 성능적으로 좋지 않았다.

  • Heap Memory Allocation,
  • Reference Counting,
  • Dynamic Dispatch

그렇다면 Struct로 "Polymorphic(다형성)"을 사용할 수 없을까? 

이는 Protocol Oriented Programming을 통해 가능하다.

 

 

Dynamic Dispatch without V-Table

Class의 경우 Common Inheritance Relationship(공통 상속관계)가 존재하기 때문에 V-Table이 필요했다.

 

하지만 Struct의 경우 Class와 달리

Common Inheritance Relationship(공통 상속관계)는 존재하지 않는다. 

따라서 V-Table이 필요가 없게 된다.

 

Protocol Witness Table (PWT)

Protocol에선 V-Table의 사용 없이

"PWT(Protocol Witness Table)"을 통해 Dynamic Dispatch을 하게 된다.

 

PWT는 Type별로 하나씩 할당되는데

이는 method의 Implementation과 link되어 있다.

 

즉, Swift에서 PWT를 통해 method의 Implementation으로 이동하여 Dynamic Dispatch가 가능하다.

 

Existential Container

Array는 고정된 Offset으로 Element들을 저장하는데,

위의 Line Point는 서로 다른 Size를 가진다. 

 

즉, 서로 다른 Size를 가질 수 있는 Protocol 타입들을 저장하기 위해

"Existential Container"라는 5word의 Storage Layout을 도입하게 된다.

이 중 3word는 valueBuffer로 reserved되어 있다. 

 

Point의 경우 2word이기 때문에 Value Buffer에 들어갈 수 있지만, 

Line의 경우 4Word가 필요하기 때문에 Value Buffer에 들어갈 수 없다. 

 

Swift는 Line과 같이 큰 Type에 대해서 Heap영역에 메모리를 할당하여 값을 저장하고, 

해당 메모리에 대한 Pointer를 Existential Container에 저장한다.

 

Value Witness Table (VWT)

"Value Witness Table(VWT)"는

Existential Container의 Lifetime을 관리하며,

타입마다 한 개씩 존재한다.

 

앞서 살펴보았듯 LinePoint는 Size가 다르기 때문에, 

메모리 할당, 해제할 때는 다른 mechanism이 있어야 한다.

VWT는 이런 다른 타입에 대해서 메모리 할당, 해제 및 Lifetime을 관리한다.

 

allocate

Protocol Type의 Local Variable의 Lifetime이 시작될 때, 

VWT내부의 allocate함수를 호출한다.

 

여기서 해당 Instance가 필요한 메모리 영역을 할당한다. 

Point와 같은 Small Type의 경우는 Heap영역이 필요 없지만,

Line은 4word가 필요하기 때문에 추가적인 Heap 메모리까지 할당한다. 

 

copy

다음은 source에서 Local Variable을 초기화하는 값을 Existential Container로 복사해야 한다.

 

Point의 경우는 Value Buffer에 저장하고,

Line의 경우 해당 값들은 Heap에 할당된 메모리 영역에 저장된다.

 

destruct

Protocol Type의 Local Variable의 Lifetime이 끝나게 되면,

VWT의 destruct 앤트리를 호출한다.

destructType에 포함된 value에 대한 Reference Count를 감소시킨다.

(Line의 경우 Reference Type을 가지고 있지 않기 때문에 필요가 없다)

 

deallocate

마지막으로 Swift는 deallocate함수를 호출한다.

Heap영역에 메모리가 할당되었다면, 해당 메모리 영역을 해제한다.

 

Existensial Container In Action

지금까지 내용을 정리하자면!

Protocol을 통해 Struct에도 Polymorphic(다형성)을 사용할 수 있다. 

하지만, Class와 다르게 공통 상속관계가 존재하지 않는다

따라서 V-Table이 아닌 PWT를 통해서 Dynamic Dispatch가 가능하다. 

 

또한, Protocol Type Local Variable의 값들은 Existential Container에 저장되며, 

이는 VWT에 의해 관리된다.

 

따라서, Existential Container는 PWT와 VWT를 알 수 있어야 한다. 

앞서 Existential Container에서 5word 중 3word는 Value Buffer로 활용된다 하였는데,

나머지 2word는 PWT와 VWT에 대한 Reference를 저장한다.

 

이제 예시를 통해 실제 코드에서 Swift가 내부적으로 어떤 동작으로 수행될지 알아보자.

 

다음과 같은 코드가 있다고 가정해 보자. 

Drawable이란 Protocol Type의 변수를 생성하고, 

drawACopy함수를 호출한다. 

이때, drawACopy에 전달된 매개변수는 

내부적으로 Local Variable을 만들어 Argument를 이에 할당한다.

 

다음은 내부적으로 어떻게 수행되는지 알아보기 위한

Existential Container의 의사코드이다. 

 

Swift는 함수에 전달된 매개변수를 Existential Container로 전달하게 되고,

다음은 Swift는 함수 내부에서 사용될 Existential Container을 Stack영역에 할당하게 된다.

 

다음은 Argument Existential Container의 VWT와 PWT를 읽어와 

이를 local에 할당한다.

 

다음으로는 Argument로 전달된 Existential Container의 값들을 

Local Existential Container의 Value Buffer에 할당한다.

 

앞서 말했듯! 이는 VWT의 allocatecopy 호출로 이루어진다.

따라서 VWT의 allocatecopy를 호출하여 Value Buffer의 값을 저장한다. 

 

만약 Large Type인 경우 Heap에 할당된 메모리 영역에 저장하고, 

Small Type의 경우 Value Buffer에 저장한다.

 

Protocol Type은 Dynamic Dispatch되기 때문에, 

PWT를 통해 실제 메서드의 Implementaion의 위치를 알 수 있게 된다.. 

 

따라서, Swift는 Existential Container가 Reference하는 PWT로 이동하여,

메서드의 Implementaion으로 jump하게 된다.

 

draw메서드는 값들을 input으로 받는데, 

이 값들은 Large Type인 경우엔 Heap에 할당된 메모리의 시작주소

Small Type의 경우엔 Value Buffer의 시작주소

전달해야 한다.

 

따라서 VWT의 projectBuffer함수는 타입에 따라 알맞은 메모리의 주소를 반환한다.

 

마지막으로 VWT의 destructdeallocate를 통해 ,

Reference Type의 Value가 있다면 Reference를 감소시키고, (destruct

Buffer가 할당되었다면 Buffer를 deallocate한다.

 

이후, 해당 함수의 동작이 끝나게 되면,

Stack영역에 할당된 Existential Container가 해제되게 된다.

 

 

Protocol Type Stored Property

다음과 같이 Protocol Type을 Stored Property로 가지고 있는 Pair Struct가 있다.

Pair는 2개의 Existential Container를 감싸는 구조이다.

앞서 살펴보았던 Line Struct는 4word가 필요하기 때문에, Heap Allocation이 발생한다.

또한, PairLine은 모두 Struct이기 때문에 총 4번의 Heap Allocation이 발생한다.

 

앞선 포스팅에서 살펴보았듯, Heap Allocation은 expensive하다.

 

만약 Line은 Class로 바꾼다면, 한 번의 Heap Allocation이 일어나지만, 

Class는 그들의 state를 공유한다. 

즉, pair.first의 값을 바뀌게 되면, pair.second, copy.first, copy.second 모두 값이 바뀌게 된다.

 

Copy And Write (COW)

해당 문제는 "Copy And Write(COW)"로 해결할 수 있다. 

위와 같이 Line의 프로퍼티들을 LineStorage라는 Class로 대체한다.

 

이렇게 되면 기본적으로 한번의 Heap Allocation만 일어날 수 있다. 

 

값의 변경이 일어난다면 move메서드 내부와 같이 Reference Count를 확인한다.

Reference Count가 1보다 큰 경우라면 state가 공유될 수 있기 때문에, 

LineStroage의 복사본을 생성하고 값을 변경한다.

 

이제 위와 같이 하나의 Heap Memory를 공유하다가, 

값이 변경되면 복사를 하는 방식이다.

 

Summary

Large Value의 경우 Copy하는 경우 Heap Allocation이 여러번 일어난다.

이를 COW를 통해 indirect Storage를 사용하게 되면 Reference Counting은 늘어나지만, 

Allocation은 줄어들게 된다.

 

 

Reference

WWDC2016 - Understanding Swift Performance

복사했습니다!