티스토리 뷰

안녕하세요 Hani입니다.

이번에는 WWDC16에서 발표된 Understanding Swift Performance에 대하여 알아볼 거예요. ☺️

 

해당 토픽이 다루는 목차는 총 5개로 이루어져 있어요

1. Allocation

2. Reference Counting

3. Method Dispatch

4. Protocol Types

5. Generic Code

 

 

이번 포스팅에서는 Protocol Types에 대하여 다뤄보려고 합니다.

 

 

지난번에는 Drawable 클래스와 그 클래스를 상속받는 Point / Line 클래스가 예시로 소개되었는데

이번에는 Drawable 프로토콜과 이를 채택하는 구조체로 바뀌었네욥.

 

물론 프로토콜을 채택하는 클래스도 가능하지만

의도하지 않은 공유가 일어날 수 있습니다. 🥺

 

 

Drawable 프로토콜을 준수한 타입은 전부 drawables에 들어갈 수 있겠죵?

 

하지만 Drawable은 클래스가 아니니까 Virtual Method Table을 이용하지 않아요.

대신 Protocol Witness Table을 이용하는데

요게 뭐냐면

 

 

프로토콜을 채택한 각 타입마다 Protocol Witness Table이 생성되는데

이 테이블은 해당 타입이 구현된 곳에 연결되고 있네욥

 

 

이렇게 배열 안에 있는 프로토콜 타입에 맞는 Protocol Witness Table에 접근하고

Protocol Witness Table에서 해당 메서드가 구현된 곳을 찾아가 호출하면 좋을 텐데

프로토콜 타입이 Point인지 Line인지 어떻게 알징 🥺

 

 

그리고 구조체라 값을 저장해야 하는데...

주소는 일정한 메모리를 차지하지만 값은 그렇지 않음 🥺

 

어떻게 프로토콜 타입마다 동일한 크기의 메모리를 할당해서 offset을 같게 만들 수 있을깡

 

 

좌좐

그래서 준비한 Existential Container.

 

Existential이란 프로토콜 타입을 의미해요.

즉, Existential Container는 프로토콜 타입의 컨테이너입니다.

 

 

컨테이너는 5 words의 용량을 가지고 있는데

이중에 3 words는 Value Buffer로 정해져 있어요.

 

 

따라서 2 words만 필요한 Point 구조체는 Value buffer에 쏙 들어갈 수 있습니다. ☺️

 

 

하지만 Line 구조체는 4 words가 필요한데요 🥺

 

이처럼 큰 타입은 Value Buffer에 들어갈 수 없으니까

힙 영역에 메모리를 따로 할당하고 이 메모리를 가리키는 주소를 Value Buffer에 저장합니다.

 

그런데 어떻게 3 words를 넘고 안 넘고를 나눠서 처리해줄 수 있을까용?

 

 

빠라밤

또 테이블이 등장했습니다.

이번에는 Value Witness Table ☺️

 

Existential Container의 네 번째 word는 Value Witness Table를 가리키고 있어욥

 

이 테이블은 프로토콜 타입 별로 하나씩 존재하게 되는데

값의 생명주기를 관리해줍니다.

 

 

생명주기를 어떻게 관리하는지 Line의 Value Witness Table을 통해 알아봅시당.

 

프로토콜 타입 인스턴스의 생명주기 시작점은 Value Witness Table의 allocate 메서드가 실행되는 거예욥.

 

Line은 4 words로, Value Buffer보다 크기 때문에 추가로 힙에 메모리를 할당하고

해당 메모리에 대한 주소를 Value Buffer에 저장합니다. 🧐

 

2 words였으면 힙 할당은 일어나지 않아용

 

 

그다음엔 Value Witness Table의 copy가 실행되어 값이 힙에 복사됩니다.

 

2 words였으면 Value Buffer에 복사되었겠죠? ☺️

 

 

다음은 인스턴스의 생명주기가 끝날 때 호출되는 destruct입니다.

값에 대한 레퍼런스 카운트를 감소시켜줍니다.

 

초록색 안에 있는 값들이 사라졌죠? ☺️

 

 

마지막으로는 deallocate를 통해 힙 메모리가 할당 해제됩니다.

 

 

여기까지 Existential Container의 세 번째 word까진 Value Buffer,

네 번째 word는 Value Witness Table을 가리키는 것을 알게 되었어용

 

 

이제 Existential Container의 마지막에는 뭐가 있는지 알아봅시당.

 

마지막 word에는 Protocol Witness Table을 가리키는 포인터가 있습니다.

 

 

등장하는 테이블이 많아서 정리하자면

프로토콜 타입별로 Protocol Witness Table과 Value Witness Table이 생성되고

프로토콜 타입의 인스턴스 별로 Existential Container가 생성됩니다.

그리고 Existential Container마다 프로토콜 타입에 맞는 Protocol Witness Table과 Value Witness Table가 연결되는 거예욥.

 

 

 

그림으로 살펴봤으니 코드로도 한 번 봐보겠습니다 ☺️

숨 꽉 참으세요

한 호흡이 되게 깁니다.. 🥺

 

 

Drawable을 순응하는 Point 구조체의 인스턴스를 생성하고

drawACopy 메서드를 호출한 모습입니다.

 

인스턴스를 생성하면 Generated Code처럼 Existential Container가 생기겠죠? 

(수도 코드입니다 ㅎ 실제로 저렇다는 건 아니고..)

 

 

drawACopy 메서드에 방금 생성한 인스턴스를 넣어서 호출하면

drawACopy에 Existential Container를 전달 인자로 넣어서 호출한 것과 같아집니다.

 

 

전달인자로 Existential Container를 받으면

새로 Existential Container 구조체 인스턴스를 스택에 할당해서 local 변수에 담습니다.

 

 

그리구 전달 인자로 받은 Existential Container로 local 변수를 초기화를 해줍니다.

 

엄.. local.vwt = vwt로 바뀌어야 하지 않나? 흠흠..

 

 

암튼 이렇게 연결을 시켜놓구 

 

 

val에 있는 값을 local로 복사를 합니다.

 

 

만약 Point가 아니라 Line이라 3 words를 초과한다면

힙 할당이 일어나고 힙 쪽에 값이 복사되겠죵? ☺️ 

 

 

Existential Container에 대한 세팅이 끝나면 local.draw()가 호출되는데

해당 인스턴스의 Existential Container가 가리키고 있는 Protocol Witness Table을 찾을 거예욥.

 

그럼 Protocol Witness Table에서 draw가 구현된 곳을 찾아서 실행할 수 있습니당

 

 

draw는 값의 주소를 전달 인자로 받길 원하는데

projectBuffer는 값이 3 words를 초과하지 않으면  Existential Container의 시작 주소를 반환하고 

 

 

그렇지 않다면 힙 할당이 일어난 곳의 시작주소를 반환합니다.

 

결국 projectBuffer를 통해 얻은 주소를 pwt.draw의 전달 인자로 넘겨주는 것으로 local.draw를 대체하는 거예요. 😎

 

 

local.draw가 끝나면 local 변수의 생명주기가 끝나므로 Value Witness Table의 destruct와 deallocate가 호출됩니다.

 

 

쨔잔

메모리가 다 지워졌습니당.

 

 

 

한 번 더 복습해볼게여.. 🥺

Drawable 프로토콜 타입 프로퍼티가 정의된 구조체네욥.

 

 

Line 구조체와 Point 구조체 인스턴스를 Pair 구조체의 생성 인자로 넘겨줬습니다.

그럼 Existential Container가 두 개 만들어지겠죠? 

 

 

Line 구조체는 4개의 프로퍼티를 가지고 있으니까 4 words를 차지합니다.

따라서 Existential Container의 Value Buffer에 전부 들어갈 수 없으니까

힙 메모리를 따로 할당하고 거기에 copy를 해줍니다.

 

Point는 쏙 들어갔네요 🥰

 

 

만약 Point 인스턴스를 Line 인스턴스로 바꿔치기하면 저렇게 바뀜!

 

 

Line 인스턴스를 두 개 넣으면 두 번 힙 할당이 일어나는데

 

 

pair를 복사하면 힙 할당 총 네 번 일어납니다 🥺🥺🥺🥺🥺 

 

 

전 포스팅에서 클래스의 인스턴스를 생성할 때 타입과 레퍼런스 카운트를 저장할 수 있는 공간을 추가로 할당했는데

만약 힙 할당할 때도 해당 공간을 만들어준다면?!

 

 

힙 할당을 추가로 하지 않아도 된다는 말씀 ☺️

 

 

대신 참조하기 때문에 의도치 않은 공유가 일어나겠죠.. 하

 

 

이렇게 두 변수가 상태를 공유하지 않도록 decouple 하면 좋을 텐데..  🥺

 

 

그래서 Copy-on-Write(CoW)라는 방법이 적용됩니다. ☺️

 

ㅁㅊㅁㅊ Line이 쳐다보기도 싫게 바뀌었습니다..

정신 차리고 뜯어보기로 해요.. 🥺🥺🥺🥺🥺

 

Line에 직접적으로 구현되었던 프로퍼티가 LineStorage라는 클래스로 묶여서 구현되어있네요.

Line 구조체가 LineStorage를 참조하게 될 테구

Line의 값을 얻으려 할 때는 Line의 LineStorage 안에 있는 값을 읽을 거예요.

 

 

값을 변경시킬 때는 먼저 LineStorage의 Reference Count를 확인해서

1보다 크면 LineStorage를 복사한 후 값을 변경시킵니다.

 

 

음... 음

 

어려우니까 예시를 봅시다. 🥺

 

 

Line 인스턴스를 생성하고 Pair의 생성 인자로 인스턴스가 들어간 모습이에요.

 

아까는 힙 할당이 두 번 일어나서 Pair의 first, second가 서로 다른 Line을 가리키고 있었는데

이번에는 같은 Line을 가리키고 있습니다.

 

 

pair를 복사할 때 힙 할당이 일어나지 않는 대신 레퍼런스 카운트가 증가한 모습 ☺️

 

 

예를 들어 copy.second의 Line을 옮기기 위해 move 메서드를 호출하면

레퍼런스 카운트를 확인하고 1보다 크면 다른 곳에서도 참조를 하기 있다는 의미니까

동일한 Storage를 새로 만들고 값을 변경하여 copy.second만 새 Storage를 참조하게 만들 수 있어요.

 

pair.first pair.second copy.first는 move를 호출하지 않으면 기존 Line을 참조하고 있겠쥬

 

 

 

 

후.. 어렵다

 

프로토콜 타입에 대하여 다시 정리해보면!

프로토콜 타입의 프로퍼티가 3 words 이하인 경우에는

Value Buffer에 쏙 들어갈 수 있기 때문에 힙 할당이 추가적으로 일어나지 않아요. ☺️

 

힙할당이 일어나지 않으니 레퍼런스 카운팅을 할 필요도 없습니다.

 

Value Witness Table과 Protocol Witness Table을 통한 dynamic dispatch로 인해 동적 다형성으로 동작할 수 있습니다.

 

프로토콜 타입의 프로퍼티가 4 words 이상인 경우에는 힙 할당이 일어나죠 🥺

프로퍼티가 참조 타입인 경우 레퍼런스 카운팅도 할거예욥..

 

 

4 words 이상일 때 힙 할당이 많이 일어나는 모습..!

(프로퍼티가 참조 타입인 경우는 고려하지 않은 그림)

 

 

단 CoW를 통한 Indirect Storage를 이용하면

4 words 이상인 경우에도 힙 할당이 덜 일어납니다.

 

레퍼런스 카운팅을 해야 하지만 힙 할당이 많이 일어나는 것보단 좋아용.

 

 

Protocol Types까지 정리해봤구

 

 

다음엔 Generic으로 찾아뵙겠습니당. ☺️

 


References

https://developer.apple.com/videos/play/wwdc2016/416/

 

댓글