이번 포스팅에선 Generic을 사용할 때 Method Dispatch는 어떻게 동작하는지,
Generic Type의 변수는 어떻게 저장되고 Copy되는지 알아보자.
Generic Method Mechanism
왼쪽은 Drawable의 Protocol Type을 파라미터로 전달받는 함수이고,
오른쪽은 Drawable Type을 만족하는 Generic Type을 파라미터로 전달받는 함수이다.
별반 다를게 없어 보이지만,
Geric코드는 Parametric Polymorphism(다형성)이라 불리는
Static한 형태의 Polymorphism을 제공한다.
즉, 오른쪽의 Generic한 함수는
왼쪽의 함수 형태보다
Static한 형태의 Polymorphism을 제공한다.
Static한 형태의 Polymorphism의 의미를 알아보기 앞서 Type Per Call Context를 알아보자.
Type Per Call Context
Type Per Call Context는 무엇인지 알아보자.
해당 코드는 Generic Parameter를 갖는 foo함수를 호출하며,
foo함수는 또다시 Generic Parameter를 갖는 bar함수를 호출한다.
해당 함수를 수행하게 되면 Swift는
foo함수의 Generic Type T를 실제 호출될 때 사용하는 Type으로 바인딩한다.
위의 경우에는 Point로 바인딩한다.
bar 함수 역시 바인딩된다.
이렇듯 Swift는 Call Chain을 따라 내려가며,
Generic Type변수를 실제 호출 시 사용되는 Type으로 대체한다.
즉, 이와 같이 Generic Type Parameter는 호출될 때의 Type으로 대체되기 때문에,
이를 "Type Per Call Context"라 한다.
Static한 형태의 Polymorphism
앞서 Generic 코드는 "Static한 형태의 Polymorphism"을 제공한다 언급하였다.
위와 같이 Genric Paremter를 가진 함수는 Polymorphism(다형성)을 제공한다.
하지만, Call Context마다 실제 사용되는 타입으로 대체되기 때문에, (Type Per Call Context)
Static한 형태로 Polymorphism(다형성)이 제공된다.
Unspecialized Generic Methods
Swift는 Generic Code에 대해 특정 경우가 만족되면
"Generic Specialization"이라는 Compile Optimization이 가능해진다.
우선 적으로 Optimization이 일어나지 않을 경우부터 살펴보자.
기존 Protocol Type Parameter를 가지는 함수는
파라미터로 Existential Container를 전달했다.
하지만, Generic Type Parameter를 "Type Per Call Context"이기에,
Existential Container를 사용하지 않는다.
Existential Container는 서로 다른 Size를 가질 수 있는 Protocol 타입들을 하나의 틀에 저장하기 위해 만들어 졌다.
하지만 Generic Type Parameter는 Call Context마다 Concrete Type으로 대체되기 때문에,
Existential Container가 필요가 없다.
하지만, 얼마나의 메모리를 할당해야 할지와 같은 정보가 필요하기 때문에 VWT는 필요하다.
또한, 사용되는 메서드들에 대한 포인터 역시 필요하므로 PWT도 필요하다.
Swift는 전달된 Paramter를
내부적으로 Local Variable을 만들어 Arugnemt를 할당한다.
Protocol에서와 같이 Local Variable만들 때 VWT를 사용한다.
Protocol Type은 Stack영역에 Existential Container를 생성했다면,
Generic Type은 3word의 Value Buffer를 Stack 메모리 영역에 할당한다.
Generic Type도 마찬가지로
Large Type의 경우, Heap에 메모리를 할당하여 저장한다.
즉, Type별로 메모리 관리가 필요하기 때문에, 이 역시 VWT에 의해 관리된다.
Specialization Optimization
앞서 말한대로 "Static한 형태의 Polymorphism"은 특정조건에서
Generic Specialization이라 불리는 Compiler최적화를 가능하게 한다.
다음 코드를 예시로 Generic Specialization이 어떻게 이루어지는지 알아보자.
Swift는 Generic한 함수는 위와 같이
Swift는 Type-Specific한 함수를 생성한다.
즉! "Generic Specialization"은 Polymorphism은 제공하면서,
Compiler가 코드내에서 사용되는 Type별로 함수를 생성하기 때문에,
Static Dispatch가 가능하다.
이로 인해 성능이 증가하게 된다.
이렇게 각 Type별로 함수가 생성된다면 코드의 사이즈 증가될 수 있냐는 의문점을 가지지만,
"Generic Specialization"은 Complier에게 더 많은 Context를 제공함으로써
Swift는 추가적인 Compiler Optimization이 가능해지기 때문에 상황에 따라서 더 짧아질 수 있다.
When Does Specialization Happen
Specialization을 통한 Optimization이 가능하기 위해선 몇가지 조건을 만족시켜야 한다.
우선, 명확한 타입추론이 가능해야 한다.
위의 경우 point 변수는 Point()로 초기화되어 있기 때문에 타입을 추론할 수 있다.
다음은 Type Definition과 Generic Method가 같이 Compile되어야 한다.
다음과 같이 Point Struct의 Definition과 Generic 함수를 따로 컴파일하게 된다면,
Generic 함수는 Point를 알 수 없게 된다.
따라서 두 파일을 하나의 단위로 즉! 한 모듈로 묶어 컴파일 해야한다.
Generic Stored Property
Line은 Value Buffer의 Size보다 크기 때문에, Heap할당이 필요했다.
또한 Line은 Struct이기 때문에 총 2번의 Heap Allocation이 필요하다.
이를 Generic으로 변경해보자
Generic의 경우 Type이 결정나면 Runtime 도중 Type이 변경되지 않는다.
위의 예시의 경우, Generic Type인 T가 Line으로 결정 났기 때문에,
아래와 같은 코드를 작성하면 Compile Error가 나게 된다.
즉, Compiler가 어떤 타입의 변수인지 안다는 뜻이다.
pair.first = Point()
// Cannot assign value of type 'Line' to type 'Point'
Protocol Type Property의 경우 하나의 특정 Type으로 결정하지 않는다.
(Line이 올수도 Point가 올수도 있기 때문이다.)
따라서, 이들을 동일한 Unifom하게 저장해줄 Existential Container를 사용했으며,
Large Type의 경우 Heap Allocation이 발생했다.
하지만, Generic Type Property의 경우 Compile Time에 특정 Type으로 결정나며,
Runtime에 변경되지 않는다.
즉, 해당 Property의 Size가 변경되지 않기 때문에 고정된 크기로 할당할 수 있게 된다.
해당 이유로 Large Type이더라도 Heap Allocation이 발생하지 않는다.
Summary
Generic Code는 Call Context마다 하나의 타입으로 결정나게 된다.
또한, 이들은 특정 조건에서 "Generic Specialization"이라는 Compiler Optimization을 가능하게 한다.
특정 조건은 아래와 같다.
- 명확한 타입 추론이 가능해야 한다.
- 사용되는 실제 타입의 구현부와 Generic Code의 구현부가 하나의 단위로 Compile되어야 한다.
Specialization
"Generic Specialization"이 일어난 Struct Type의 경우,
Copy시에 Heap Allocation은 발생하지 않으며,
Reference Counting도 필요하지 않다.
또한, 앞서 살펴보았듯 사용하는 타입별로 함수를 만들어내기 때문에,
Static Method Dispatch가 가능하다.
"Generic Specialization"이 일어난 Class Type의 경우
Heap Allocation이 발생하고, Reference Counting 역시 필요하다.
또한, 사용하는 타입별로 함수를 만들어 낸다 해도,
내부 메서드를 호출할 때는 V-Table을 참조해야 하므로 결국 Dynamic Dispatch가 된다.
Unspecialization
Specialization되지 않는 Generic의 경우
작은 값의 경우 위에서 살펴보았듯이 Stack의 Value Buffer에 fit하므로 Heap Allocation은 발생하지 않는다.
또한, PWT를 통한 Dynamic Dispatch가 수행된다.
Stack의 Value Buffer에 fit하지 않는 경우 추가적인 Heap Allocation이 발생한다.
이 역시, PWT를 통한 Dynamic Dispatch가 수행된다.
Struct Type에는 상속이 없지만,
Protocol과 결합하여 Polymorphism을 추가할 수 있다.
하지만 Protocol과 결합하면 Dynamic Dispatch가 이루어지기 때문에 성능적인 면에선 손해를 보게 되었다.
이는 Generic과 결합하게 되면 Static한 Polymorphism을 제공하기 때문에,
Specialization 최적화가 이루어진다면
Polymorphism을 제공함과 동시에 Static Dispatch가 가능해지면서 성능적으로도 개선할 수 있게 된다.
Reference
'iOS > Swift' 카테고리의 다른 글
[WWDC] Understanding Swift Performance(2) - Protocol Programming (0) | 2023.08.07 |
---|---|
[WWDC] Understanding Swift Performance(1) - Dimensions of Performance (0) | 2023.08.06 |
[Swift] Properties(2) - Wrappers, Type (0) | 2023.01.18 |
[Swift] Properties(1) - Stored, Computed, Observer (0) | 2023.01.17 |
[Swift] Memory Leak(2) - Closure의 [weak self] (0) | 2022.11.26 |