본문 바로가기
WWDC

WWDC 16 - Understanding Swift Performance

by vapor3965 2021. 6. 22.

목차

     


    • 세션보면서 정리한 내용입니다. 해석이 잘못된 경우가 있을수있으니 발견하시면 댓글로 남겨주시면 감사하겠습니다🙏🏻

     


    관련내용

    • strucct, class, protocol, generic 이 swift에서 어떻게 구현되는지 알아보자.
    • 다른dimensions 퍼포먼스에서 상대적비용들을 배워보자.
    • 어떻게 이러한정보들을가지고 어떻게적용할지보자 .

    👍요약

    • 함수가호출될때는, 우선적으로 지역변수들의 크기들이 할당된다는저어엄~
    • 스택메모리는 스택포인터의 감소증가를 통해 메모리가관리된다는점~
    • 힙메모리에할당할때는 공간을찾는비용보다, 멀티쓰레드를 고려하여 락또는 동기화과정이 비용이크다는점~!
    • string은 내부적으로 힙메모리에저장된다는점~
    • 인스턴스들은 힙,스택 에저장되며, 레퍼런스카운팅이적용될수도 아닐수도있고, dynamic, static dispatch가 적용된다.

    컴파일러측면

    • 컴파일러가 정보가많아질수록, 즉 가시성이높을수록 inlining 및 최적화여지가 가능해진다.
      • 인라닝을통해 콜스택 오버헤드가 없어지므로 빨라진다.
    • generic은 struct와 사용되면서, 또는 타입을 바로알수있는경우, 또는 whole module optimization을 통해 generic specialization이라는 최적화를 이루어, 타입을바로 사용할수있개해준다 - 각타입별메소드가생성되어진다

    타입

    • 각 타입들은 static memory에 저장되어진다.
    • protocol을 채택하는경우 각 타입별 protocol witness table이만들어진다.
    • classs는 V-table이 만들어진다.

    struct

    • 힙할당 및 레퍼런스카운팅 오버헤드가없다.
    • 컴파일러에게 가시성을 많이제공하여 최적화여지가많다.
    • protocol을 채택하는경우 각 타입별 protocol witness table이만들어진다.

    class

    • 힙할당 및 레퍼런스카운팅 오버헤드가존재한다.
    • 기본적으로 dynamic dispatch이며, 각 타입들은 V-table 로만들어진다.
    • 해당 인스턴스는 프로퍼티뿐만아니라, 추가적으로 2wrod ( V-table포인터, refCount)를 가진다.
    • V-table포인터를 통하여 dynamic dispatch가 가능해진다.

    protocl

    • dynamic 다형성을제공해준다.
    • 👍protocol을 채택하는경우 각 타입별 protocol witness table이만들어진다.
    • protocol타입을 사용하는 경우 Existential Container를 사용한다.
      • 이 EC는 valuebuffer 3word크기로가지며, 이것보다 큰 값이나 참조타입은 힙메모리에할당된다.
    • 👍generic과 결합된다면 static dispatch가 적용될수있따.

    generic

    • 👍static 다형성을제공해주며 ( 런타임때바꿀수없음 )
    • 기본적으로 protocol witness table 또는 V-table을 사용하여 dynamic dispatch를 한다.
    • 타입에대한 정보를 알수있다면 ( whole module optimization 키거나 , ) 컴파일러에의해 generic specialization을 통해 각타입별 메소드가생성? 될수있다.
    • 이러한 최적화를 통해 static dispatch가 적용될수잇다.
    • 👍그래서 메소드에 프로토콜타입 또는 수퍼클래스를 받기보다는 generic을 사용하는게 더 좋다라고 생각한다.

    static dispatch

    • 컴파일타임때 정확히 호출할수있다.
    • static dispatch는 컴파일타임때 정확히 어떤메소드를 호출할수있을지알수있으므로 컴파일러가 가시성을 갖게되면서 inlining 또는 다른최적화여지를 줄수있따는점~!
      dynamic dispatch
    • 런타임때 호출할 부분을 찾도록한다.
    • dynamic dispatch는 컴파일러에게 가시성을주지못하지만, 다형성기능을 제공할수있다는점~!

     

     

     

     


    swift의 퍼포먼스를 잘사용하기위해서는 우선, 그근본적인 내부구현을 이해할필요가있다.

     

    메모리측면과, 생성된코드들에 대해고려하자.

     

    퍼포먼스 차원
    Allocation

     1. 내 인스턴스가 stack에저장될까 heap에저장될까
    Reference Counting

     2. 인스턴스를 전달할때 얼마나많은 레퍼런스카운팅 오버헤드가발생할까
    Method DIspatch

     3. 인스턴의 메소드를호출할때, static, dynamically하게 dispatch될까?

     

     

     

    Stack 메모리구조

    • 간단한메모리구조다.
    • 포인터를유지함으로써 push, pop만을함으로써 메모리를관리한다.
      • 👍스택끝에있는포인터를 스택포인터라부른다.
    • 👍함수를호출하여 메모리를할당할때는 스택포인터가 단순하게감소한다.
    • 👍함수가끝나면, 함수가호출되기전의 포인터로 이동하기위해 증가되어짐으로써 메모리를해제한다.
    • 스택메모리,포인털르잘몰라도, 단순히, 거의상수시간에 가능한 겁나빠른 메모리구조다.

    Heap 메모리구조

    • 스택보다는 덜 효과적이지만 더 다양하다.
    • 스택이할수없는 동적인 라이프타임을 갖게해주다
      • 그러기위해서는 더 advanced된 데이터구조가필요하다
    • 👍힙에할당하기위해서는 힙메모리구조안에서 충분한사이즈가 요구되는 사용되지않은 곳을 찾는다.
      • 머, 힙찾는거는 주된비용은아니다.
    • 👍멀티쓰레드는 같은시간에 같은공간에할당할수있다 그러므로, 온전하게 보호될수있도록 락또는 동기화메커니즘을적용한다.
      • 이것이큰 비용이다.

     

    아래의 함수가 호출되어졌을때, 함수내의 코드가 실행되지않아도, 우선 각 객체들이필요한 메모리공간을 stack에 할당한다.

    • 맞아. 함수호출공부할때 먼저 스택포인터를 줄이더라고!!

    이제, 이니셜라이저가적용되면서, 각 객체를초기화해준다.


    이렇게함수가끝나면, 오른쪽과같이 스택포인터가 늘어난걸볼수있다!

    • 역시나, 함수호출배우면서알수잇듯이, 값이 그대로남아있는걸볼수있다.

    class일떄

    • class이여서 실제 저장되는값이 stack에 저장되지않고, 메모리의 참조를 스택에저장할것이다.
    • 마찬가지로, 함수가시작되면, 우선적으로 스택공간을 차지한다. ( 스택포인터를감소)

    • 그다음 우선적으로 힙에 락을걸면서가능공간을 찾고, 초기화한다.
    • 여기서주의할점은, 클래스는 힙메모리에 4word 크기를 할당한다.
    • 👍struct는 2word였지만, 클래스는 실제프로퍼티뿐만아니라, 우리대신 swift가 관리하기위해 따로 2word공간을 할당한다. - 아마 레퍼런스카운트및, vtable참조하는변수아닐까?

    클래스이므로, 복사하지않고, 같은메모리를 가리키도록한다.

    함수호출이끝나면, 우리대신 힙을 락하고, 반환하고,


    스택도 반환한다.


    그러므로 class는 identity, indirect storage , unintended sharing 라는 특성을가진다.


    그러므로, 이러한특성이필요없다면 struct를사용하라

    color색, orientation - Tail위치, Tail - tail이어떤모양일지

    • 이러한 balloon은 메세지에사용될이미지이기때문에 매우빨라야한다.

    • 그래서 캐싱레이어를 만들었당 
    • 하지만, key로써 String은 적합하지않다. 왜냐하면 내가단순히 “dog”으로넣을수있으니까,안전하지못하다.
      • 또한string은 다양한 캐릭터들을 포함하느데, 이것은 간접적으로 heap에저장된다
      • 그러므로, 캐시가 힛되더라도, heap할당을 요한다.

    • 그래서대신 struct를 사용하라, 훨씬 더 안전하다.
    • 또한 struct는 dictionary 의 key로써 사용이가능하다.
    • 그러므로, 캐시힛일어나면 힙할당오버헤드가존재하지않다

     

     

    어떻게 swift는 힙에할당된것을 언제 해제해도 안전하다라는 것을알까?

    • 그것은 swift는 힙에저장된 인스턴스들은 레퍼런스카운팅을 가지고있다. 그러므로 강한참조하거나 해제하면 증가되고감소된다 . 그러므로0 이된다면 swift는 해제해도안전하다라는것을알수있다

    이러한 레퍼런스카운팅ㅇ은 굉장히자주일어난다

    • 다양한 indreiction이있고,
    • thread safety를 고려해야하고, ( 동시에 증가할때 atomicaly하게 증가시켜줘야한다)

    실제작성한코드를 비교하면 클래스는 refCount를 가지며,

    • 힙메모리에는 refCount가 저장딘다

    만약 혼합된 경우이면? struct안에 class

    • string은 내부적으로 힙에할당하고, UIFont는 클래스이다.
    • 즉 복사하게되면 내부 클래스개수만큼(비례해서) 래퍼런스카운팅발생한다


    그러므로, 클래스를사용할필요가없다면, 최대한 프로퍼티들을 struct로 대체가능하도록만들어랑

    • 한개의클래스정도는괜찮다.

     

     

     

    DIspatch

    • 컴파일타임때 정확히어떤 메솓르ㅡㄹ호출할지안다면 static dispatch.
    • 런타임때 알수잇다면 dynamic dispatch.

    Static Dispatch

    • 컴파일타임에 알수있고, 런타임때 정확히 구현을 찾아서들어간다.
    • inlining 또는 다른 최적화에 여지를줄수있다.
      • 👍즉 컴파일타임때 이미 알수잇으므로, 보여질수있으므로 컴파일러가 최적화할수있께된다. ( inlining과같이. 그냥아예 코드를삽입해버리는거)

    dynamic dipatch

    • 컴파일타임에 어떤 구현을 정확히 호출할지모른다.
    • 그러므로 런타임때 직접구현을찾아들어간다.
    • static dispatch보다 한단계더많으므로 그렇게 큰 비용은아니다.
    • 👍하지만! dynamicdispatch는 컴파일러의 가시성을 막아준다. 왜냐, 컴파일때모르니까!
      • 그렇기때문에 inlining, 다른 최적화여지를 막아버린다.

    iniling이뭘까?

     

    아래와같이 있을때, drawaPoint, draw는 static dispatch이다.

    정확히 어떤메서드를 ㅎ출할지알수있기때문에, 아래와같이 바뀔수있따.



    또한 심지어, draw()코드내용이 삽입되어질수있다!!!


    👍그러므로, 아래와같이 따로 함수로 점프해서가지않고, 코드를바로실행하게된다

    • 그러므로👍 콜스택(함수호출)의 오버헤드가 따로없게된다.
    • 스택함수호출하고, 다시분해,돌아오고, ( 스택포인터증가,감소 돌아오고, 등등)이럴필요가없어진다.

    • 뭐, 하나의 static, 하나의 dynamic과 비교했ㅇ르때는 큰 차이느없지만,이게엄청많다면 큰 차이를준다.

     

     

     

     

    그럼그럼그럼, 왜 dynamic dispatch가 필욯라까!!!!

     

    왜냐! polymorphism을 가능하게해주니까! ( 다형성

    Drawble 부모클래스를 Point, Line이 서브클래스했다

    • Drawble, Point, Line은 클래스이기때문에, 배열을만들면 각원소들을 같은사이즈로 만들수있다.

    • 여기서직관적으로알수있는게, d.draw()에서 d는 어떤타입인지모른다.
    • 👍그렇기때문에, 컴파일러는 인스턴스에 각 인스턴스가 어떤타입인지에 대한 포인터를 저장한다!! ( type 파랑색 )
      • 👍이포인터는 어떤타입인지를 가리키는데, 이것은 static memory에 저장되어있다.( 데이터인듯 ?
      • 👍즉, 클래스인스턴스는 자기프로퍼티외에 추가적으로 2word를저장하는데, 각각 refCount, type포인터이다.
      • 👍그러므로, 이해당함수를 호출할때, type으로, 해당 XXX.Type을 찾는다. 이 XXX.Type은 virtual method table이라고 불린다.

    클래스는 기본적으로 dynamic dispatch이다.
    즉 모든클래스는 dynamic dispatch를 요구하지않는다.
    Final Class

    • 상속되어지지않는다면 final 키워드를 사용하여, static dispatch되도록해라

     

    그러므로, 이인스턴스가 힙에저장될까, 스택에저장될까, 고민.
    인스턴스를전달할때 얼마나많은 레퍼런스카운팅이일언날까
    인스턴스의 메소드가 dynamic이냐, static으로호출되냐,

     

     

     


    자자, 그러면 struct는 어떻게 다형성을 적용할까? 이것은 프로토콜지향프로그래밍으로될수있다.

     

     

    Protocol type

     

    상속없이 다형성을 구현해보자.

    protocol을 채택하는 Point, Line struct가 있다.
    하지만 struct는 상속이없기때문에, 🤔어떻게 [Drawable]배열은 정확하게 어떤 인스턴스의메소드를 호출할수있는걸까? 왜냐하면 struct는 상속도없고, vtable을 참조하는 포인터도없기때문이다. ( 프로퍼티만큼의 데이터만저장하기땓문!)

    protocol witness Table


    그답은! - protocol witness Table 이라고불리는 메카니즘기반의 테이블이다

    • 각 타입마다 하나의 pwt 를 갖는다.
    • 또한 pwt안에있는 entries는 타입이실제로구현한곳을 링크시켜준다.

    1. 🤔오케이! pwt를통해서 실제구현한곳을 찾는다라는건 알겠어! 하지만 어떻게 배열안에서 pwt를 찾느냐말이야. ( 밑에 배열안에 ? ? 물음표가 그것. ?에 뭐가들어가냐이거야~ )

     

    🤔그뿐만아니라, 배열안에는 각 동일한사이즈가있어야하는데, Line 인스턴스는 4word, Point는 2word를 갖자나, 이를 어떻게해결할건데????????


    Existential Container

     

    그답은두구두구두구둑 바로 Existential Container 이다.
    swift는 이러한 container를 사용한다.


    첫번째 3word는 valueBuffer이다.

    Point 인스턴스는 2word만 갖기때문에 촥~ 얹혀질수있다.


    Line 인스턴스는 4word이니까, 어떠컈?
    swift는 힙에메모리를 할당하고, 값을저장하고, 그 가리키는 포인터를 valueBuffer에저장한다


    🤔또다른문제점 Line , Point인스턴스는 다르기때문에, Existential Container는 이를 어떻게 관리하는가?

     

     

    Value Witness Table


    그답은 Value Witness Table 이라는 메카니즘기반의 테이블이다.

    • 각 value의 life time을 관리한다.
    • 이 테이블또한 각 타입마다 하나에 존재한다.

    Value Witness Table 동작

    • 우선, 메모리를 allocate한다. Exitentail Container에 .

    • 그다음 외부로부터 받은 값을 EC에 복사한다.


    • 그다음 끝날때즘이면 값을 삭제해주고,


    • 메모리를 해제한다.

    그리고 ec는 VWT의 포인터를 가지고있다.

    마찬가지로 PWT 의 포인터를 가지고있다.

    Existential Container 동작과정

    • drawACopy는 지역변수를 생성한다.
    • 즉, ExistentailContainer를 생성한다.
    • 우선 메모리를 할당한다.

    • 그다음에 VWT, PWT 포인터를할당한다.


    • 그다음 값을복사한다.

     

    • pwt 에서 draw메서드를 찾고, 점프하여호출한다.


    • 그다음에 함수가끝나면, VWT destruct ,dealllocate .

     

     

     

     

    protocol 타입이 struct안에 저장되는경우는 어떨까?

    • 두개의 Existentail Contianer가 저장이된다.


    * 이것은 다른타입들도 저장하게해준다!


    • Pair struct를 복사하면 아래와같이 복사되고, 총 4개의 힙할당이일어난다.

    • 만약, Line을 클래스로 구현한다면, 아래와같이 참조하는 형태가된다. 덕분에 힙할당은 줄어든다는 장점이있다.
      • 그렇지만 참조타입으로인해, second.x1 = 3하면 first도 변경이된다
      • 그럼,.. 힙할당도 줄이면서, second.x1도 유니크하게변경하기위해서는 어떻게해야할까


    • Copy and write 기법을 적용하도록한다.
    • Line은 struct하고 하고, 내부저장되는 실질적인값을 class로 구현한다.
      • 그리고, 값이 변경되는 순간에? 레퍼런스카운트를 확인하고, 2이상이라…
      • 여튼 참조하고있다가, 변경되는순간에 새로 값을 할당한다.

    • 그러므로, 아래와같이 힙할당은 한번만일어나게되고, 굉장히 싸다.

     

     

     

    Protocol & Small Value

    • value buffer보다 작은 값들은 빠르게저장되고,
    • struct가 클래스를참조하고있지않다면 힙할당이없고,
    • protocol witness table을 통해 dynamic dispatch



    Protocol & Large Value

    • value buffer보다 큰 값들은 힙할당이일어난다.

    Protocol 요약

    • Witness Table과 Existential Container에 의해 dynamic 다형성을제공해준다.
    • valuebuffer보다 큰 값은 힙할당을요한다

     

     

     

     

     


    Generic

    • drawACopy() 를 Generic으로바꾸보자

    • 프로토콜과 무슨차이가있을까?
    • Static 다형성을 제공한다.
    • One type per call context

    • 아래와같이 Generic을 이용하면, Protocol, value witness table을 사용한다.
      • 대신 Existential container는 사용하지않는다.

    • 그럼 프로토콜 테이블을 사용하지않고 사용할수없나욘?
    • Generic Specialization 이 가능해준다.
      • 👍타입을 유추할수있게끔 보여져야하기때문에, 한모듈안에있지않다면, 최적화가 일어나지않겠지만, Whole module optimization을 통해 전체모듈을 하나로봄으로써 가시성이생기고, 가능해진다.
    • Generic은 static 다형성으로, 현장에서 타입을 바로사용한다.

    • 그리고, 전달되어진 타입을통해, 특정타입의 메소드가새로만들어진다.
      • 즉, swift는 메소드가 타입별로 생성되어질수있다.


    • 엄ㅊ어빠르긴해서좋겠지만, 그럼 코드가 너무방대해지는거아니가요?
      • 하지만,ㅋ 컴파일러는 최대한 중복되는걸 줄임으로써 최적화해준다.
    • 타입은 런타임떄 바뀌지않는다.
    • 다시한번, 아래는 프로토콜을 사용한경우, 힙할당이일어난다.


    • 대신 Generic을 사용하면 genric sepcialization이 되어, 더이상힙에할당하지않게된다!!
      • 아주cool!!
      • 대신, 런타임때 타입이변경되지않는다.
        • 이것이우리가의도한경우이면 아주좋다.


    그러므로, Struct Type과 Generic은,

    • 힙할당이 일어나지않고,
    • 레퍼런스카운팅이없고,
    • static method dispatch로, 컴파일러가 최적화여지를 준다.
    • static 다형성을 제공한다.

    class Type 과 Generic은

    • 힙할당이일어낙,
    • reference카운팅이일어나고,
    • V-table을통해 dynamic dispatch가일어난다.
    •  

    Small Value( struct갰지? ) 와 Unspecialized Generics 은

    • 값이작기때문에 3word buffer에들어가고,
      • 그렇기때문에 레퍼런스카운티이없고
    • 대신, 원래대로 protocol witness table을 통해 dynamic dispatch가 적용된다.

    Large Value와 Unspecialized Generics 은

    • 힙할당,
    • 레퍼런스카운팅
      • dynamicdispatch.

    요약

    • struct는 값타입으로 컴파일러에게 다양한 정보를제공함으로써 최적화여지를 준다. 그럼으로써 런타임이빨라진다.
    • class는 identity나 OOP적인 다형성을 사용하고싶을때 사용하라,
      • 추가적으로 레퍼런스카운팅을 줄이는 기술도있다.
    • generic은 static 다형성으로,
    • protocol 은 dynamic 다형성을제공해준다 ( 프로토콜타입 배열처럼 )
    • 프로토콜타입을 사용하는경우, 너무큰value인경우에는 힙할당이많이일어나므로, struct ( class ) 의 copy and write 기능을 구현하여 이용하라.

     

    'WWDC' 카테고리의 다른 글

    WWDC 15 - Mysteries of Auto Layout, Part 2  (0) 2021.06.23
    WWDC 18 - High Performance Auto Layout  (0) 2021.06.23
    WWDC 20 - What's new in Swift  (0) 2021.06.22
    WWDC 19 - Modern Swift API Design  (0) 2021.06.22
    WWDC 19 - Optimizing App Launch  (0) 2021.06.22

    댓글