Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Understanding Swift Performance(2/2) #17

Open
kimscastle opened this issue May 12, 2023 · 0 comments
Open

Understanding Swift Performance(2/2) #17

kimscastle opened this issue May 12, 2023 · 0 comments
Assignees

Comments

@kimscastle
Copy link
Contributor

kimscastle commented May 12, 2023

Protocol types

클래스는 상속을 이용해 다형성을 구현할 수 있다. 그러나 구조체는 어떻게 다형성을 구현할 수 있을까? 답은 바로 프로토콜 이다.

dynamic dispatch 의 예제를 다시 보도록 하자.

image-20220813110500907

  • 이번엔 프로토콜을 사용해 Drawable 을 정의했다.

  • Drawable 이란 프로토콜을 채택하면 draw 메소드를 실행할 수 있어야 한다.

  • 구조체로 정의된 Point 와 Line 타입이 Drawable 프로토콜을 준수하도록 했다.

  • 이번엔 프로토콜인 Drawable 타입을 배열로 관리하는 drawables 변수가 존재한다.

그러나 이전과 다른 부분이 있다면, 바로 Line 과 Point 는 구조체이기 때문에, 상속을 이용하지 않아 v-table disaptch 를 수행할 수 없다는 것이다. 그렇다면 Swift 는 어떻게 프로토콜 타입 배열을 돌면서 알맞은 draw 메소드를 호출할 수 있을까?

PWT (Protocol Witness Table)

애플리케이션에서 프로토콜을 구현하는 타입 하나당 Protocol Witness Table 을 가진다. 그리고 PWT 에는 구현 타입에 대한 정보들이 들어가있다.

image-20220813111335641

이런 PWT 를 사용해 실제 구현 메소드를 찾아서 disaptch 할 수 있다. 그러나 여전히 다음과 같은 두 개의 질문이 남는다

  • 어떻게 배열의 요소에서 PWT 테이블을 참조할까?
  • Line 과 Point 구조체를 살펴보자. Line 은 4 개의 프로퍼티가 존재하고, Point 는 2 개의 프로퍼티가 존재하기 때문에 같은 크기를 가지고 있지 않다. 그렇다면 어떻게 배열에 똑같은 크기의 Element 들을 저장할 수 있을까?

Existential Container

Swift 는 Protocol type 의 정보들을 저장하기 위해 Existential Container 을 사용한다

스크린샷 2022-08-13 11.17.43

Existential Container 에는 기본적으로 3 word 만큼의 크기를 가지는 Inline Value Buffer 를 가지고 있다. Point 와 같이 두 개의 프로퍼티, 즉 두 word 만큼의 크기만 필요한 작은 타입들은

image-20220813111910939

위와 같이 Existential ContainerValue buffer 안에 들어갈 수 있다. 그러나 Line 구조체는 4 개의 프로퍼티, 즉 4 개의 word 가 필요한 것을 볼 수 있다.

Swift 는 이렇게 Inline Value Buffer 에 들어가지 않는 프로토콜 구현 타입을 Heap 에 할당하고, Existential Container 에는 Heap 메모리에 대한 참조를 저장한디.

즉 Line 과 Point 는 필요한 메모리 크기가 다르고, 프로토콜 타입은 Existential Container 를 사용해 관리되는 것을 배웠다. Line 은 Heap 메모리에 저장되고, Point 는 Existential Container 의 Inline Value Buffer 에 충분히 저장되기 때문에, 이 차이점을 어떻게든 관리해야 한다.

Value Witness Table (VWT)

Value Witness Table 은 Value 의 life time, 즉 할당과 복사, 파괴를 관리하며, 타입별로 VWT 를 하나씩 가진다.

Existenstial Container 와 VMT 가 어떻게 동작하는지 알아보기 위해서 지역 변수로 선언된 Line 구조체의 life time 을 살펴보자!

지역 변수인 Line 구조체가 할당될 때, Line VWT 내부의 allocate 함수를 호출한다.

image-20220813112954296

이 Line 이라는 구조체는 4개의 프로퍼티를 가지고 있고, 따라서 Existential Container 에 들어가지 않기 때문에 Heap 에 인스턴스 메모리 할당이 필요하다.

image-20220813113054039

그 다음으로 실제 인스턴스의 값을 Existential Container 로 복사해야한다. 그러나 Line 은 Existential Container 의 Inline value buffer 에 맞지 않으므로, 이 값을 Heap 에다 copy 한다.

image-20220813113253648

이제 프로그램이 계속해서 실행되고, Line 구조체의 사용이 끝나 메모리에서 해제해야 한다.

이 단계에서 Line 의 VWT 는 destruct 를 호출하고, destruct 는 Line 혹은, 다른 타입과 관련된 타입들에 대해 reference count 를 감소시킨다. Line 은 내부에 참조 타입 프로퍼티를 가지지 않으므로 이런 일은 발생하지 않는다.

image-20220813113458507

그리고 마지막으로 deallocate 를 호출해 Heap 에서 인스턴스의 메모리를 해제한다.

image-20220813113523295

Existential Container 정리

Existential Container 의 3 word 는 Inline Value buffer 이고, 나머지 2 word 는 VWT (Value Witness Table) 을 위한 reference 와 PWT (Protocol Witness Table) 을 위한 reference 로 이루어져있다.

image-20220813123957837

PWT 는 실제 구현에 대한 정보를 담고 있는 Table, 그리고 VWT 는 프로토콜 타입의 값의 life cycle 을 관리하는 table 이다. 아래에서 예제를 통해 더 자세히 알아보자

Example of Protocol type

image-20220813131526790

  • Drawable 프로토콜을 준수하는 val 이란 지역 상수를 선언하고, Point 인스턴스를 할당헀다.
  • Drawable 프로토콜 타입을 받는 drawACopy 함수에 Point 인스턴스를 넘겨 호출한다.

image-20220813131825705

위 예제에서는 이해를 돕기 위해, Existential Container 을 Swift 의 구조체 형태로 표현했다. Existential Container 는 3 개의 Value Buffer 를 가지고, VWT, PWT 에 대한 참조를 가진다.

image-20220813132233645

함수가 실행되면, Swift 는 drawACopy 의 argument 로 existential Container 를 전달한다.

그리고 함수가 실행되면, 해당 매개변수에 대한 지역 변수가 만들어지고, argument 가 할당된다.

image-20220813132304515

이제 함수가 진짜 실행되면, Stack 에 Existential Container 를 할당하고, Existential container 로부터 VWT 와 PWT 를 읽어온다. 그리고 함수 내 선언된 지역 변수의 필드를 초기화한다.

image-20220813132742902

Point 의 VWT, 와 PWT 는 모두 Heap 에 할당되어 다음 그림과 같은 메모리 구조를 보여준다.

image-20220813133227737

그리고 이제 실제 인스턴스의 값을 Existential Container 에 할당하는데, 여기서 Point 는 두 개의 프로퍼티만 가지고 있기 때문에, Container 의 Existnential Container 에 충분히 들어갈 수 있다. 그러나 Line 의 경우엔 4 개의 프로퍼티를 가지기 때문에, 실제 값은 Heap 영역에 할당되고, Value buffer 에는 해당 힙 주소에 대한 참조값이 들어가게 된다.

image-20220813133405092image-20220813133422523

이제 drawACopy 상의 지역 변수는 PWT 를 참조해, 실제 Point 에 구현된 draw 메소드를 실행시킨다.

image-20220813133633653

마지막으로 함수가 종료되어 지역변수를 해제해야 하는 시점이 오면, VWT 의 deallocate 엔트리가 호출되고, 관련된 값들의 reference count 를 줄인 후에, 메모리에서 해제된다.

image-20220813133651204

정리

지금까지는 프로토콜 타입을 준수하는 구조체에서 도대체 어떤 방식으로 다형성이 구현되는지를 알아보았다. 구조체는 기본적으로 상속이 안 되기 때문에 static dispatch 방식을 사용한다. 그러나 구조체 또한 프로토콜을 채택해서 다형성을 지원하게 된다면, 프로토콜의 메소드는 dynamic dispatch 를 통해 호출하게 된다.

  1. Swift 는 Existential Conatiner 를 Stack 에 만든다. -> 이 Existential Conatiner 에는 3 word 의 Inline Value buffer, 그리고 PWT, VWT 에 대한 레퍼런스를 가지고 있다.
  2. VWT와 PWT 는 Heap 에 할당되고, VWT 는 Value 의 life cycle 을 관리하고, PWT 는 프로토콜 구현체의 정보에 대해 담고 있다.
  3. Point 와 같이 3개 이하의 프로퍼티를 가지는 Existential Container 의 Inline Value Buffer 안에 충분히 들어가기 때문에 Heap 할당이 일어나지 않지만, Line 과 같은 것들은 Heap allocation 이 일어나고 Existential Container 는 이 Heap 인스턴스 주소에 대한 참조를 저장한다.
  4. draw 프로토콜 메소드를 실행할 때, PWT 를 조회해 draw 메소드를 호출함으로써 함수 호출이 마무리된다.

Protocol Type Stored Properties

image-20220813134644924

하나의 예를 더 보도록 하자. Pair 라는 구조체가 존재하고, 두 개의 Drawable 프로토콜 타입 프로퍼티를 가지고 있다.

image-20220813134756807

두 개의 프로퍼티를 가지고, 이것은 전부 프로토콜 타입이기 때문에, 2개의 Existential Container 가 Stack 에 생성되었고, 또한 이것은 pair 라는 구조체 인스턴스 안에 있는 것을 볼 수 있다.

또한 first 프로퍼티는 Line 을 가지고 있고, second 프로퍼티는 Point 를 가지고 있기 때문에, 메모리 구조는 아래와 같이 된다.

스크린샷 2022-08-13 15.18.12

  • Line 은 Inline buffer 를 초과했기 때문에 Heap 에 할당된 것을 주목하라
  • Point 는 두 개의 프로퍼티를 가지고 Inline Value Buffer 안에 들어가기 때문에, Existential Container 안에 값이 복사되었다.
  • 두 개의 Line 인스턴스로 Pair 를 생성한다면, 아래와 같이 Heap 할당이 두 번 일어나게 된다.

image-20220813152127932

copy 라는 새로운 지역변수를 선언하고, 여기에 pair 를 할당해 값을 복사해보자.

image-20220813152214571

Line 은 구조체이기 때문에, 값을 복사한다고 했을 때, 새로운 Existential Container 가 생길 것이다. 왜냐하면 프로토콜 타입이기 때문에, 그러나 Line 은 4개의 프로퍼티를 가지고 있기 때문에, 여기서 Heap 할당이 두 번 더 일어나게 된다. (Copy on Assign)

Line 이 그냥 구조체일 경우, Stack 영역에 인스턴스가 할당되기 때문에 그렇게 큰 문제는 되지 않겠지만, 프로토콜을 채택한 구조체고, 프로퍼티가 4개라서 Existential Container 의 Value Buffer 를 초과하기 때문에 Heap allocation 이 계속해서 발생하는 점을 주목하자.

Class 타입으로 변경해 Heap allocation 을 해결해보자.

Line 을 구조체 대신 클래스로 변경하면 어떻게 될까? 클래스는 값이 복사될 때 새로운 인스턴스가 할당되는 것이 아니라 주소값을 참조하고, reference count 를 증가시킨다.

또한 클래스는 참조 타입이기 때문에, 어차피 값은 Heap 에 할당되고, Existential Container 에는 메모리 주소만 들어가게 된다.

즉 새로운 변수를 선언해 값을 할당하면 다음과 같은 메모리 구조를 갖게 된다.

스크린샷 2022-08-13 15.28.59

값을 복사할 경우 Heap allocation 이 일어나던 구조체와 달리, 클래스 타입은 레퍼런스 카운트의 증가만 일어난다. 따라서 이런 경우엔 구조체 대신 클래스를 사용하는게 성능상 이점이 있다고 말할 수 있겠다.

하지만 Class 를 사용하는 경우 의도치 않은 상태 공유가 일어날 수 있다.

여러 변수가 상태를 공유한다는 것은, 항상 의도치 않은 버그로 이어질 수 있다.

image-20220813153133890

위 그림과 같이, first 인스턴스의 x1 값을 바꾸면 같은 인스턴스를 참조하는 second 역시 바뀐 값을 바라보게 된다. 우리가 Value semantics, 즉 이런 상태 공유를 원하지 않을 때는 어떻게 할까?

Copy-on-Write

Copy on write 라는 기법은, 할당할 때 바로 복사하는 COA 방식이 아니라, 값을 할당할 때는 같은 인스턴스의 주소값을 가르키고 있다가, 값의 변경이 일어날 때 비로소 새로운 인스턴스를 할당해주는 방식을 말한다.

Swift 에선 Dictionary, Set, Array 같은 컬렉션 타입에 대해서 COW 를 지원해주는데, 원한다면 우리가 정의한 구조체에 대해서도 이러한 복사를 구현할 수 있다. COW 를 구현한다면, 값을 복사할 때마다 Heap allocation 이 일어나지 않고 값을 수정하려 할 때 비로소 Heap allocation 이 일어나기 때문에 성능이 좋아진다고 할 수 있다.

정리

  • Protocol type 은 Existential Container 를 가지고, PWT, VWT 를 활용해 다형성을 지원한다.
  • Existential Container 는 3 words 를 수용할 수 있는 Value Buffer 를 가지고 있다. 프로토콜 구현 타입이 3개 이하의 프로퍼티를 가진다면 Value Buffer 안에 값을 복사할 수 있기 때문에 Heap allocation 이 일어나지 않지만, 만약 4개 이상의 프로퍼티를 가지고 있다면 Heap allocation 이 일어나게 된다.
  • 따라서 만약 4개 이상의 프로퍼티를 가지는 프로토콜 타입의 경우 class 로 만들어주는게 Heap allocation 을 줄여주므로 상태 공유와 성능을 trade off 했다고 볼 수 있다.
  • 하지만 상태공유를 원하지 않는 경우 COW 를 직접 구현해 상태 공유까지 없앨 수 있다.

Generic code

  • 이번에는 제네릭타입의 변수가 저장되고 복사되는 방식과 메서드디스패치를 함께 연관지어서 확인해본다
스크린샷 2023-05-12 오후 1 15 09
  • drawACopy 메서드는 Drawable이라는 프로토콜을 채택한 어떤 타입이라도 input으로 넘겨받을수있는 부분만 변경된 상태이고 나머지 부분은 동일하다 이때, 프로토콜 타입과 비교했을때 어떤부분이다른걸까
스크린샷 2023-05-12 오후 1 17 41
  • generic코드는 매개변수 다형성이라고 하는 static한 다형성을 지원한다(하나의 context당 하나의 type)
  • 예시에서는 Drawable이라는 프로토콜을 채택하는(== 제약조건을 가진) 매개변수를 취하는 foo라는 함수가 있고 이함수는 받은 매개변수를 bar라는 함수에 전달한다 bar라는 함수는 generic 매개변수 T를 사용한다 그런다음에 이 프로그램은 Point를 만들어서 이 point를 foo함수에 전달한다
스크린샷 2023-05-12 오후 1 22 28
  • 이 함수가 실행될때 Swift는 T를 Point에 바인딩한다 foo함수가 이 바인딩으로 실행되고 함수가 호출되면 Point를 갖는다
스크린샷 2023-05-12 오후 1 23 50
  • 결국 이 호출된 context의 generic 매개변수 T는 Point타입으로 바인딩이되고 이 예제에서 처럼 타입은 매개변수를 따라 아래로 대체되어간다 이것이 static한 다형성을 의미한다
스크린샷 2023-05-12 오후 1 28 38

프로토콜 타입을 사용했을 때와 마찬가지로 하나의 공유 구현이 있다
그리고 이 공유 구현은 이전에 프로토콜 타입에 대해 했던 코드와 매우 유사하게 보인다

스크린샷 2023-05-12 오후 1 29 00

이것은 protocol 및 value witness table을 사용하여 일반적으로 해당 함수 내부의 작업을 수행한다

스크린샷 2023-05-12 오후 1 29 10

그러나 호출 컨텍스트당 하나의 타입만 있기 때문에 Swift는 여기서 existential container를 사용하지 않는다

스크린샷 2023-05-12 오후 1 29 19

대신, 이 호출에서 사용된 Point 타입의 value witness table과 protocol witness table을 함수에 대한 추가 인자로 전달할 수 있다.
따라서 이 경우 Point 와 Line에 대한 value witness table이 전달 되었음을 알 수 있다

스크린샷 2023-05-12 오후 1 29 43

그런 다음 해당 함수를 실행하는 동안, 매개변수에 대한 로컬 변수를 생성할 때 Swift는 value witness table을 사용하여 잠재적으로 힙에 필요한 버퍼를 할당하고 할당 소스(source of assignment)에서 목적지로 복사를 실행한다

스크린샷 2023-05-12 오후 1 29 54

그리고 로컬 매개변수에 대해 draw 메서드를 실행할 때와 유사하게, 전달된 protocol witness table을 사용하고 테이블 내의 고정 오프셋의 draw 메서드를 찾아 구현으로 이동한다

스크린샷 2023-05-12 오후 1 30 02

여기에는 existential container가 없다. 그렇다면 Swift는 이 지역 매개변수를 위해 생성된 지역 변수에 대해 필요한 메모리를 어떻게 할당하는지 알아보자

스크린샷 2023-05-12 오후 1 30 36

바로, 스택에 valueBuffer를 할당한다. valueBuffer는 3 word이다

스크린샷 2023-05-12 오후 1 31 27

Point와 같은 작은 값은 valueBuffer에 맞는다
Line과 같은 큰 값은 다시 힙에 저장되고 local existential container 내부에 해당 메모리에 대한 포인터를 저장한다
그리고 이 모든 것은 value witness table 사용을 통해 관리된다
이제 "여기에서 프로토콜 타입을 사용하지 않았을 수 있었을까" 라는 질문을 던질 수 있는데
이 정적 형태의 다형성은 제네릭의 특수화라고 하는 컴파일러 최적화를 가능하게 한다고 한다

스크린샷 2023-05-12 오후 1 32 04

여기에 generic 매개변수를 사용하는 함수 drawACopy가 있으며 해당 메서드를 호출하면서 함수에 Point를 전달한다
그리고 우리는 정적 다형성을 가지고 있으므로 호출 site에 하나의 타입이 존재한다
Swift는 해당 타입을 사용하여 함수의 generic 매개변수를 대체하고 해당 타입에 고유한 해당 함수 버전을 생성한다
따라서 여기에 Point 타입의 매개변수를 사용하는 Point 함수 drawACopy가 있으며 해당 함수 내부의 코드는 다시 해당 타입에 대해 고유하다는것을 알 수 있다

스크린샷 2023-05-12 오후 1 32 41

Swift는 프로그램의 호출 시점에서 사용되는 타입별로 다른 버전을 생성한다
따라서 Point와 Line에 대해 drawACopy함수를 호출하면 해당 함수의 두 가지 버전으로 맞춤화하고 생성할수 있지만 코드 많이 늘어날 가능성이 있다. 그러나 사용할 수 없는 정적 타이핑 정보는 적극적인 컴파일러 최적화를 가능하게 하기 때문에 Swift는 실제로 잠재적인 방향으로 코드 크기를 줄일 수 있다

스크린샷 2023-05-12 오후 1 34 11

예를 들어 Point 메서드 함수의 drawACopy를 인라인하고 그런 다음 코드에 더 많은 컨텍스트가 있으므로 코드를 추가로 최적화한다.
그래서 함수 호출은 기본적으로 이 한 줄로 줄어들 수 있고 Kyle이 우리에게 보여줬듯이 이것은 draw 구현으로 훨씬 더 줄일 수 있다
이제 Point 메서드의 drawACopy가 더 이상 참조되지 않으므로 컴파일러는 이를 제거 하고 Line 예제에 대해 유사한 최적화를 수행할 수 있다. 따라서 이 컴파일러 최적화가 반드시 코드 크기를 증가시키는 것은 아닌것이다

스크린샷 2023-05-12 오후 1 35 32

위의 코드는 Drawable 프로토콜 타입 쌍을 갖고 있는 상태인데

스크린샷 2023-05-12 오후 1 36 17

Pair를 만들고 싶을 때마다 실제로는 같은 타입의 쌍, 예를 들어 Line 쌍 또는 Point 쌍을 만들어서 input으로 넣어줬다

스크린샷 2023-05-12 오후 1 37 35

이제 한 쌍의 Line에 대한 storage에는 두 개의 힙 할당이 필요 하다. 이 상황에서는 제네릭 타입을 사용할 수 있다

스크린샷 2023-05-12 오후 1 38 04

따라서 쌍을 제네릭으로 정의한 다음 해당 제네릭 타입의 first 및 second 프로퍼티가 이 제네릭 타입을 갖도록 하면 컴파일러는 실제로 동일한 타입의 쌍만 생성 하도록 강제할 수 있다. 이렇게 하면 나중에 프로그램에서 한 쌍의 Line 묶음에 Point를 저장할 수 없게 된다. 그러나 이것은 성능이 더 좋을까 나쁠까?

스크린샷 2023-05-12 오후 1 39 42

저장 프로퍼티가 generic 타입일때, 런타임에 타입을 변경할 수 없다.
generated code가 의미하는 바는 Swift가 enclosing 타입의 저장소를 인라인으로 할당할 수 있다는 것이다.
따라서 Line 쌍을 만들 때 Line에 대한 메모리는 실제로 enclosing 쌍의 인라인으로 할당된다.
추가 힙 할당이 필요하지 않다. 나중에 다른 타입의 값을 저장프로퍼티에 저장할 수도 없다.

스크린샷 2023-05-12 오후 1 40 20
  1. generated code는 본질적으로 이 함수를 구조체로 작성한 것과 같기 때문에 구조체 타입을 사용하는 것과 동일한 성능 특성을 갖는다.
  2. 구조체 타입의 값을 복사할 때 힙 할당이 필요하지 않다.
  3. 구조체에 참조가 포함되어 있지 않으면 참조 카운팅이 없다.
  4. 또한 컴파일러 최적화를 추가로 가능하게 하고 런타임 실행 시간을 줄이는 정적 메서드 디스패치를 가지고 있다.
스크린샷 2023-05-12 오후 1 41 02
  1. 이를 클래스 타입과 비교해보면 클래스 타입을 사용하면 힙 할당 및 인스턴스 생성, 값 전달을 위한 참조 카운팅 , V-Table을 통한 동적 디스패치같은 클래스와 유사한 특성을 갖게 된다.
  2. 특수화되지 않은 generic 코드를 살펴보자.
스크린샷 2023-05-12 오후 1 41 35
  1. 스택에 할당된 valueBuffer에 작은 값이 들어맞기 때문에 지역 변수에 힙 할당이 필요하지 않다.
  2. 값에 참조가 포함되지 않은 경우 참조 카운팅도 없다.
  3. 그러나 witness table을 사용하여 모든 잠재적 호출 site에서 하나의 구현을 공유하게 된다.
스크린샷 2023-05-12 오후 1 42 32

큰 값과 generic 코드를 사용하는 경우 힙 할당이 발생한다.
간접 저장 기술을 해결 방법으로써 사용 할 수 있다
큰 값에 참조가 포함된 경우 참조 카운팅이 생기고, 동적 디스패치의 힘을 얻는다. 즉, 코드 전체에서 하나의 일반 구현을 공유할 수 있다.
그래서 우리는 오늘 구조체와 클래스의 성능 특성이 어떻게 생겼는지, generic 코드가 어떻게 작동하는지, 프로토콜 타입이 어떻게 작동하는지 보았다.

Summary

스크린샷 2023-05-12 오후 1 42 39

동적 런타임 타입 요구 사항이 가장 적은, 엔티티에 적합한 추상화를 선택하자.
이렇게 하면 정적 타입 검사가 가능하고 컴파일러는 컴파일 타임에 프로그램이 올바른지 확인할 수 있다.
또한 컴파일러는 코드를 최적화하기 위한 더 많은 정보를 가지고 있으므로 더 빠른 코드를 얻을 수 있다.
따라서 구조체 및 열거형과 같은 값 타입을 사용하여 엔티티를 표현할 수 있다면 value semantic 체계를 얻을 수 있으며 이는 의도하지 않은 상태 공유가 없고 최적화 가능한 코드를 얻을 수 있다.
엔티티에 있어, 또는 객체 지향 프레임워크로 작업을 해야해서 클래스를 사용해야 하는 경우 Kyle은 참조 카운팅 비용을 줄이는 몇 가지 기술을 보여주었었다.
프로그램 일부를 보다 정적인 형태의 다형성을 사용하여 표현할 수 있는 경우 generic 코드를 값 타입과 결합할 수 있으며 매우 빠른 코드를 얻을 수 있으면서도 해당 코드에 대한 구현도 공유할 수 있다.
그리고 Drawable 프로토콜 타입 예제의 배열과 같이 동적 다형성이 필요한 경우 프로토콜 타입을 값 타입과 결합하여, 클래스를 사용하는 것보다 비교적 빠른 코드를 얻을 수 있으면서도 여전히 value semantic을 유지할 수 있다.
그리고 프로토콜 타입 또는 제네릭 타입 내부에서 큰 값을 복사하기 때문에 힙 할당에 문제가 발생하는 경우 해당 기술, 즉 Copy on Write와 간접 저장을 사용하여 이 문제를 해결하는 방법을 보여줬었다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant