티스토리 뷰

반응형

안녕하세요 :) Zedd입니다.

이렇게 길어질 줄이야!!!! 오늘은 이 WWDC 세션의 마지막인 Generic을 배워보도록 하겠습니다.

물론 Generic의 개념은 아니고 Generic의 성능을 배울 것 같네요.



Understanding Swift Performance (Swift성능 이해하기) - (3) 




자, 이렇게 drawACopy에 제약을 걸어주면, 

즉 Drawable을 준수하는 타입만 파라미터로 들어올 수 있다고 제약을 줘봤습니다.

위 코드는 근데 결국



이거랑 별반 다를게 없죠.



그럼 말이죠. 뭐가 다른걸까요?? 


Generic코드는 parametric polymorphism라고도 하는

static한 형태의 polymorphism(다형성)을 지원합니다.


띠용


이게 무슨소리인;;


우리가 함수 foo를 가지고 있다고 생각해볼게요. 이 함수는 Generic으로 만들어졌고 Drawable만 받을 수 있쬬.

Point는 Drawable을 준수했으니, foo의 파라미터로 들어갈 수 있겠죠? 

자, 이 foo함수가 실행되면, Swift는 Generic타입 T를 Point타입에 바인딩합니다.

함수 foo가 이 바인딩과 함께 실행될 때, bar가 호출되면, local변수는 직전에 발견된 타입인 Point를 가지게 됩니다.

위에서 봤듯이, 타입은 매개변수를 따라 호출체인으로 대체됩니다.



이것은 좀 더 정적인 형태의 polymorphism이나 parametric polymorphism을 의미합니다.


흠...이해 가시나요??

Generic이 조금더 "static"스럽다고 언급한 것을 제 나름대로(?)해석해보면, 타입 T가 (여기서는) Point로 "대체"된다고 그랬잖아요? 뭔가 static dispatc같은 느낌으로 그런 느낌으로 안다는 거 아닐까요!?!?!?!?

아직모름


더 봅시다.

다시 drawACopy로 돌아와봅시다.



역시 Generic코드입니다.

위에서는 Point를 파라미터로 전달하네요. 


drawACopy안에서 local.draw()를 부르네요! 하지만 저번시간에 배웠듯이, 이 draw가 누구꺼의 draw냐를 알기 위해서는 Protocol/Value Witness Table을 사용해야합니다.



근데 여기서는 하나의 호출컨텍스트 당 하나의 타입(지금은 Point)만 있어서 Swift는 여기서 existential container를 사용하지 않는다고 해요.

(헉,.....ㅎ....또 헷갈...무조건 만드는게 아니라니..이게 말이되나요)


대신, 이 call site에서 추가적인 argument로써 Point의 VWT와 PWT를 전달합니다. (오..)



그런 다음, 해당 함수를 실행하는 동안, 매개변수에 대한 local 변수를 만들면, Swift는 witness table을 사용하여 필요한 모든 버퍼를 Heap에 할당하고, 원본에서 destination으로 복사본을 만듭니다.


그리고 draw메소드를 실행하면, 전달된 pwt를 사용하여 table에서 고정 오프셋의 draw메소드를 찾아 구현으로 jump합니다. 


자...아까도 말했다시피 이 컨텍스트에서 existential container는 없습니다.

그럼 Swift는 local argument에 대해 어떻게 메모리를 할당할까요? (let local = Point())



stack에 valueBuffer를 할당한다고 합니다. 이 valueBuffer는 공간이 3개입니다.

Point같이 작은 value는 valueBuffer에 맞지만, 



Line은 valueBuffer에 맞지않으므로 Heap에 저장되고, local existential container내부에 해당 메모리에 대한 포인터를 저장합니다.

그리고 이 모든것은 vwt를 사용하기 위해 관리됩니다.


흠 지금 이해가 안가시는 분들도 계실 것 같은데..물론..저를 포함해서요..existential container가 필요없다고 했으면서 마지막에는 "local existential container내부에 해당 메모리에 대한 포인터를 저장합니다."라고 왜 말하지...?


저번글에서의 existential container는 vwt와 pwt를 가리키는 공간도 existential container안에 있었는데 이번에는 없나보네요. 

그럼 저게 existential container인건지..그냥 stack에 valueBuffer를 할당한건지........

왜 existential container가 없다고 말했지?? 



땀땀;;;;

자,..아무튼 계속 봅시다. 

자, 우리가 이렇게 어쩌구 저쩌구 했는데 이게 더 빠른가요? 

이 방법(Generic)이 더 나은가요?? 

그냥 파라미터에 프로토콜 타입을 주면 안되나요??


자!!! 이 static한 형태의 polymorphism은 Generic의 specialization라고 불리는 컴파일러 최적화를 가능하게 합니다.



drawACopy가 또 등장했는데요, Point를 파라미터로 호출했습니다.

우리는 static polymorphism을 가지고 있고, call site에는 하나의 타입만 있습니다.



Swift는 해당 타입을 사용하여 함수에서 Generic매개변수를 대체하고, 해당 타입 버전의 함수를 만듭니다(OptimizationTips에서도 나온 이야기군요)

 그럼 Line을 호출하면



이렇게 Line버전의 drawACopy가 생기는 것이죠.


하지만, 이건 code size를 증가시킬 가능성이 있습니다. 타입마다 해당 함수의 버전을 만드니까요.

하지만, aggressive compiler optimization(적극적인 컴파일러 최적화.?)가 가능하기 때문에 Swift는 실제로 code size를 줄일 수 있습니다.





짠!!!!!

방금 우리는 specialization이 동작하는 방식을 봤어요.

그럼 질문을 하나 할 수 있겠죠. 이 specialization는 언제발생하는지!??!?


예제를 하나 보도록 합시다.



Point라는 struct를 만든다음, Point의 인스턴스를 drawACopy의 파라미터로 호출하네요. 

Swift는 여기서 이 코드를 specialization하기 위해서 이 call site에서 타입을 추론 할 수 있어야 합니다.


가능하죠! 왜냐하면 point가 Point()로 초기화되었으니까요.

그렇기때문에 Swift는 여기서 타입 추론이 가능합니다.

Swift는 또한 specialization과정에서, 사용된 타입과 Generic기능 자체를 사용 할 수 있는 함수를 정의해야합니다.

여기서도 마찬가지입니다. 


자, 일단 Point라는 struct 가 지금 main.swift라는 한 파일안에 정의된 것 보이시죠? Point정의를 다른 파일로 옮겼다고 가정해봅시다.



이제 두 파일을 개별적으로 컴파일하면, UsePoint.swift파일을 컴파일 할 때, 컴파일러에서 두 파일을 따로 컴파일하기 때문에, Point정의를 더이상 사용 할 수 없습니다. 그러나 whole module optimization을 통해, 컴파일러는 두 파일을 하나의 단위로 컴파일하고, Point.swift파일에 대한 insight를 가지게 되며, 최적화가 수행될 수 있습니다.



==> whole module optimization안할거면 한 파일에 둬라..?같음....


저번 글에서 Pair sturct기억나시죠?



Line의 경우, valueBuffer에 fit하지 않기 때문에 Heap할당이 필요했고, 별도의 indirect storage도 없으니 Heap할당이 2번 일어나겠네요.

그럼 여기서 Generic을 사용해봅시다.



이렇게 하면 아까랑 똑같죠? Generic으로 바꾼 것 뿐...

하지만, 동일한 속성의 타입만 pair로 만들 수 있도록 강제했죠.

Pair(Point(), Line())은 안된다는 뜻.


한번 봅시다.

일단, 타입을 런타임에 변경 할 수 없다는 것을 기억해주세요.



저번글에서 말했듯이, 2개의 Line 인스턴스가 Pair라는 타입에 묶이게 되죠. 


여기서 궁금증이 들 수 있죠. 제가 든 궁금증인데 왜 Line인데 왜 Heap에 할당안됐냐

왜 3개라면서 왜 4개 들어갔냐??

모냐???장난하냐????

라고 생각할 수 있겠지만..........


 specialization을 통해, Pair라는 struct의 T를 Line으로 바꾸어서 그런가 아닌가..싶네요.

그러면 existential container가 필요없고, 바로 stack의 inline에 프로퍼티들을 저장 할 수 있게 되죠.


이제 그럼 위에서 막 existential container가 필요없다고 한 이유도 감이 잡히죠.

왜냐하면 Swift는 specialization를 통해 해당 타입 버전으로 함수를 만들기 때문에 굳이 해당 메소드가 누구껀지 저장할 pwt가 필요없어지죠.

왜냐? 그냥 static dispatch하면 되거등 ㅇㅇ


그래서 여분의 Heap할당도 필요없어지게 됩니다.


 

struct타입의 값을 복사할 때, Heap할당이 필요하지 않으며 레퍼런스가 포함되지 않은 경우, 레퍼런스 카운팅이 필요하지 않습니다.

그리고 위에서 언급했다시피 새로운 버전의 메소드를 만들어내기 때문에 (타입당 하나) static method dispatch가 가능해지죠.

그래서 성능이 굉장히,,좋ㅇ,,,


class타입과 비교해봅시다. 



class이기 때문에 Heap할당, 레퍼런스 카운팅, vtable을 통한 dynamic dispatch가 가능해지기 때무네,...

흠 근데 class && generic으로 짜도, static dispatch는 안되나보군


Generic을 사용하지 않은 작은 값!

그럼 existential container를 사용 할테고, 작은 값이니까 valueBuffer에 들어맞을 거고, 들어맞으니까 Heap할당이 필요없을거고, existential container에 pwt를 통해서 메소드를 호출해야하겠죠!!!!!


하지만 큰값이면 Heap할당이 필요하고, existential container에서는 해당 Heap을 가리키는 포인터를 저장하게 되고, 즉 레퍼런스 카운팅이 있게 되고 역시 small value와 마찬가지로 pwt를 통해 메소드를 조회해야함.



그래ㅔ서....정리를 하자면....!!!!



우리는 동적 런타임 요구사항이 가장 적은 엔티티에 대해 적합한 추상화를 선택해야합니다.

이렇게 하면 static 타입 검사가 가능해지며, 컴파일러는 컴파일 타임에 프로그램이 올바른지 확인 && 코드를 최적화 하여 더 빠른 코드를 얻을 수 있도록 할 수 있습니다.

따라서 struct와 enum와 같은 value type을 사용하여 프로그램에서 엔티티를 표현 할 수 있다면, value semantic을 얻을 수 있습니다.

이는 의도하지않은 상태 공유가 아니며, 매우 최적화 된 코드를 얻을 수 있습니다.


좀 더 static한 형태의 다형성을 사용하여 프로그램의 일부를 표현 할 수 있다면, Generic코드를 value type과 결합 할 수 있으며, 이는 정말 빠른 코드를 얻을 수 있지만 해당 코드의 구현을 공유 할 수 있습니다.(but share the implementation for that code.)

그리고 Drawable프로토콜 예제와 같이 동적인 다형성이 필요하다면, 프로토콜을 value type과 결합하면, class를 사용하는 것보다 비교적 빠른 코드를 얻을 수 있습니다. 하지만 여전히 value semantic에 머물겠죠.


자, 이렇게 WWDC 2016 Understanding Swift Performance를 다 보았는데요,

이거 보고 OptimizationTips를 보니까 이해가 2배로 잘가네요.

그니까 안보면 안댐





반응형