yyeonjju / Interview_Questions

0 stars 0 forks source link

Generic을 활용한 protocol + struct의 성능 개선 #12

Open yyeonjju opened 5 months ago

yyeonjju commented 5 months ago

이것도 WWDC 2016 Understanding Swift Performance 세션과 연관되는 글이다.

struct + protocol 를 사용하면 단순히 struct를 사용하는 것에 비해 성능이 안좋아진다고 했는데 이 타입을 파라미터로 전달한다고 가정했을 때 어떻게 하면 성능이 좋을 수 있을까? 정답은 제네릭함수 사용하는 것!



제네릭함수 사용하지 않았을 때
```swift // Drawing a copy protocol Drawable { func draw() } func drawACopy(local : Drawable) { local.draw() } let line = Line() drawACopy(line) // ... let point = Point() drawACopy(point) ```
제네릭함수 사용했을 때
```swift // Drawing a copy using a generic method protocol Drawable { func draw() } func drawACopy(local : T) { local.draw() } let line = Line() drawACopy(line) // ... let point = Point() drawACopy(point) ```



그냥 프로토콜 타입으로 전달한 것 VS 제네릭타입을 써서 제네릭함수로 정의한 것 이 부분을 중점적으로 비교해서 제네릭함수를 사용했을 때 대체 왜 성능이 좋아지는 알아보자



parametric polymorphism, static form of polymorphism

foo, bar function의 예시

func foo<T: Drawable>(local : T) {
 bar(local)
}
func bar<T: Drawable>(local: T) { … }
let point = Point()

foo(point)

// foo<T = Point>(point)
     // bar<T = Point>(local)



이 과정이 바로 parametric polymorphism, static form of polymorphism 이처럼 제네릭 함수를 호출하면 호출하는 시점에 타입이 바인딩되고 그로인해 내부에서는 특정된 타입이 사용되면서 dynamic(동적)이 아닌 static(정적)으로 동작할 수 있는 것이다!



Swift가 내부적으로 static polymorphism을 구현하는 방법

이게또… 제네릭 함수 한 번의 호출에 하나의 타입만 전달(One type per call context)하는 경우에만 해당되네..??

한 번의 호출에 하나의 타입만 전달(One type per call context)이거에 집중해야해!

이 경우에는 existential container를 사용하지 않는다

그럼 existential container없이 어떻게 메모리를 할당하고 내부 메서드 실행하는가?

PWT/VWT는 추가 인자로 전달
로컬 변수를 파라미터로서 만들고 값을 저장시키려면 VWT의 allocate을 통해 stack에 할당을 해야한다
추가적인 인자로 PWT, VWT 를 함께 전달하기 때문에 local.draw()를 호출할 때에도 전달된 PWT를 참조하여 호출
발표자료에서는 저장프로퍼티를 저장하는데 이 사진을 썼는데..
existential container 없다고 했고 valueBuffer 도 없을텐데 왜 이렇게 표현했는지 모르겠고,뒤에서 나오는 그림에서는 값의 크기에 상관없이 다 stack 내부에 저장하는데 왜 저런 자료를 보여줬는지 잘 모르겠다..



그래서 이게 제네릭 안 쓴 버전보다 빠른게 확실해? Is this any faster? Is this any better?



static form of polymorphism은 제네릭의 specialization이라고 불리는 컴파일러 최적화를 가능하게한다.


그래서 specialization 가 뭔데? 어떻게 하는건데?


이렇게만 보면 코드 사이즈가 증가한다고 생각할 수 있지만




specialization은 언제, 어느조건에서 발생해? Whole Module Optmizatione(WMO)는 뭐야?



Pair 구조체 initializer 관련 예시코드

Pair (제네릭 타입이 아닌 initializer)

코드
```swift protocol Drawable { func draw() } struct Point : Drawable { var x, y: Double func draw() { } } struct Line : Drawable { var x1, y1, x2, y2: Double func draw() { } } // Pairs in our program struct Pair { init(_ f: Drawable, _ s: Drawable) { first = f ; second = s } var first: Drawable var second: Drawable } let pairOfLines = Pair(Line(), Line()) let pairOfPoint = Pair(Point(), Point()) ```


Pair (제네릭 타입인 initializer)

코드
```swift // Pairs in our program using generic types struct Pair { init(_ f: T, _ s: T) { first = f ; second = s } var first: T var second: T } let pairOfLines = Pair(Line(), Line()) // ... let pairOfPoint = Pair(Point(), Point()) ```
yyeonjju commented 5 months ago

specialization가 된다는 것을 SIL코드를 통해 알 수 있다


방법(예시)
```swift //Generics.swift @inline(never) func replaceNilValues(from array: [T?], with element: T) -> [T] { return array.compactMap { $0 == nil ? element : $0 } } let numbers: [Int?] = [32, 3, 24, nil, 4] let filledNumbers = replaceNilValues(from: numbers, with: 0) print(filledNumbers) // [ 32, 3, 24, 0 , 4 ] let floatNumbers: [Float?] = [32.0, 3.0, 24.0, nil, 4.0] let filedFloatNumbers = replaceNilValues(from: floatNumbers, with: 0) print(filedFloatNumbers) // [ 32.0, 3.0, 24.0, 0.0 , 4.0 ] ``` ``` //터미널에 $ swiftc Generics.swift -O -emit-sil -o Generics.s ``` 같은 디렉토리에 새로 생긴 컴파일러가 번역학 Generics.s 파일을 열어보면 replaceNilValues(from:with:)가 **specialization**된 코드를 볼 수 있다. ⇒ 제네릭 타입이 전달된 argument에 따라 구체적인 타입(Int or Float)으로 바뀐 것을 볼 수 있다. ``` // specialized replaceNilValues(from:with:) ... bb0(%0 : $Array>, %1 : $Int): ... //// specialized replaceNilValues(from:with:) ... bb0(%0 : $Array>, %1 : $Float): ... ```
drawACopy 제네릭 함수의 실행으로 specialized가 수행된 SIL 코드 ⇒ 지금까지 설명할 것처럼 제네릭 코드가 specialize된다는 것을 눈으로 확인해볼 수 있다.
```swift //GenericPratice.swift import Foundation protocol Drawable { func draw() } struct Point : Drawable { var x, y: Double func draw() { print("Point 인스턴스의 draw메서드") } } struct Line : Drawable { var x1, y1, x2, y2: Double func draw() { print("Line 인스턴스의 draw메서드") } } func drawACopy(local : T) { local.draw() } let point = Point(x: 1.0, y: 1.0) drawACopy(local:point) let line = Line(x1: 2.0, y1: 2.0, x2: 3.0, y2: 3.0) drawACopy(local:line) ``` ``` //터미널에 $ swiftc GenericPratice.swift -O -emit-sil -o GenericPratice.s ``` 만들어진 GenericPratice.s 파일에 들어가서 확인해보면 아까 예시코드와 다르게 여기는 Specialized라는 키워드는 없지만 Point.draw(), Line.draw() 로 나눠져서 실행된걸 보면 제네릭 타입이 각각 실행됐을 때 전달된 argument의 타입으로 구체화 되어서 각각따로 코드로 변하고 실행되는 것을 알 수 있다. ``` //Point.draw() ... //Line.draw() ... ```
yyeonjju commented 5 months ago

제네릭 타입이 아닌 initializer와 제네릭 타입의 initializer가 뭐가 다를까??

지금까지 우리가 제네릭을 살펴본 이유는 성능이 얼마나 좋아지는지를 알아보기위해서라는 걸 잊으면 안된다!

프로토콜 준수하는 구조체를 전달한다고 했을 때 → Specialized Generics는 얼마나 성능이 좋아지는가!



그냥 struct



프로토콜준수하는 구조체를 파라미터로 가지는 함수



프로토콜준수하는 구조체를 파라미터로 가지는 제네릭 함수

값의 용량이 크냐 적냐도 상관없음!



그렇다면 제네릭함수인데 클래스를 타입으로 받는 함수

결과적으로는 class에서 generic을 사용했을 때 specialization이 된다고 하더라도 일반적으로 class를 사용했을 때와 다르지 않은 성능을 보여준다.

yyeonjju commented 5 months ago

참고 글

Swift ) (3) Understanding Swift Performance (Swift성능 이해하기) [Swift] Generic에서 Method Dispatch 마법 같은 Swift 제네릭 이야기