멘토님께서 옵셔널이라는것을 직접 구현해보라고 과제를 주셨을때
생각나는건 제네릭으로 만들어 보는 것이였다
옵셔널이란 Null이 들어올수 있는 특정 타입이라는 것이니
제네릭으로 받은 타입이 Nil이야? 그럼 nil을 반환해~!
제네릭으로 받은 타입이 문자나 숫자나 아무튼 존재하는 타입이야? 그럼 그타입을 리턴해~! 였다
// CustomStringConvertible : 객체의 커스텀 문자열 표현을 제공하는 프로토콜
enum MyOptionalNew<T> : CustomStringConvertible {
case typeNil
case typeTrue(T)
init(_ value: T?) {
self = value != nil ? .typeTrue(value!) : .typeNil
}
var description: String { //CustomStringConvertible 필수로 값을 바꿔 내보내는 변수
switch self {
case .typeNil:
return "MyOptionalNew(nil)"
case .typeTrue(let value):
return "MyOptionalNew(\(value))"
}
}
}
그렇게 만든것.. 멘토님은 초기화 구문을 if let을 써서 간결성을 주도록 수정하고
제네릭 스페셜라이제이션을 좀더 학습해보면 좋을 것 같다고 이야기해주셨다
그래서 학습해보는 제네릭 스페셜라이제이션!
제네릭 스페셜 라이제이션 이란 제네릭이란 말에서 유추할수 있게 타입에 관련 된 말로 특정 타입에 대해 제네릭 동작을 다르게 처리하거나 최적화하는 기법이다
C언어에서의 제네릭 스페셜라이제이션을 사용할때 사용하는 방식으로 코드 템플릿을 특수화해서 코드의 재사용성을 높이고 특정 타입별로 최적화된 구현을 제공한다.
하지만 스위프트에서는 템플릿 스페셜 라이제이션을 직접 제공하지 않고
다른 개념들을 통해 제네릭 스페셜라이제이션을 한다
제네릭 타입에 조건을 추가해서 특정 타입에서만 제공되는 동작을 설정하여 제네릭이 처리할 타입을 제한하도록 하는 것이다
이 제한하도록 하는 제약을 이용해서 제네릭이 특정 프로토콜을 준수하는 타입이나 상속 관계에 있는 타입에 대해서만 사용되도록 조건을 거는건데
타입 안정성을 보장하고 조건을 통해서 제어를 용이하도록 한다
Where을 사용해서 하거나 : 를 사용해서 특정 프로토콜만 받아오도록 한다.
// 프로토콜 생성
protocol sayHello { func hello() -> String }
//프로토콜 채택한 구조체
struct IamSay: sayHello {
func hello() -> String {
return "안녕하세요 접니다"
}
}
func sayHelloToYou<T>(item: T) where T: sayHello {
print(item.hello())
}
// 이 함수는 제네릭 함수로 제네릭 함수는 매개변수의 타입을 T라는 타입 매개변수로 받아서 어떤 타입이든 처리할 수 있도록 일반화한다
// 이 item은 sayHello 라는 프로토콜을 따르기에 hello라는 함수가 존재할수 밖에 없다
let iam = IamSay()
// 구조체의 인스턴스 생성
sayHelloToYou(item: iam) // "안녕하세요 접니다"
//sayHelloToYou는 sayHello 라는 프로토콜 타입만 item에 들어올수 있는데
// IamSay 는 sayHello 를 채택한 프로토콜이니 들어갈수 있다
: 를 사용 하려고 한다면
func printValue<T: sayHello>(_ value: T) {
print(value.hello())
}
이렇게 하면 가능하다.
프로토콜 확장은 프로토콜에 기본 동작을 입력해서 그 프로토콜을 채택하면 그 기본동작을 사용하거나 추가 기능을 넣을 수 있도록 한다
// 프로토콜 생성
protocol sayHello { func hello() -> String }
// 프로토콜 확장 (기본 구현 제공)
extension sayHello {
func describe() -> String {
return "hello~!"
}
}
//프로토콜 채택한 구조체 1
struct IamSay: sayHello {
func hello() -> String {
return "안녕하세요 접니다"
}
}
//프로토콜 채택한 구조체 2
struct youSay: sayHello {}
// hello 함수를 수정하지 않아 "hello~!"가 리턴됨
이렇게 확장 기능을 통해 내부 기능을 꾸미지 않더라도
기본 기능이 존재하도록 하는 것이다.
템플릿 특수화를 사용하지 않지만
제네릭 타입이 특정 타입에 대해 별도로 최적화된 동작을 하도록
제네릭 제약과 프로토콜 확장을을 통해서 제네릭 스페셜 라이제이션을 구현한다
제네릭 코드가 특정 타입에 대해 최적화해서 동적으로 사용이 되도록 하는건 알겠는데 알겠는데 모르겠어요...!
새로운 과제로 컴파일러에서의 제네릭 스페셜라이제이션이 어떻게 동작할지 찾아보라고 하셨다
일부 언어에서는 제네릭 코드들을 특정 타입에서만 컴파일하는 것이 아니라 모든 타입들을 하나의 로직에서 처리를 한다
그래서 런타임에서 그 코드의 타입을 유지하며 사용할 때에 다형성을 가지고 동적으로 처리를 하지만 성능이 떨어진다는 단점이 존재한다
컴파일 시에 하나의 코드가 아닌 제네릭 타입마다 별도의 코드를 생성한다
(List< Int>와 List이 있으면 Int와 String 각각에 대해 별도의 함수와 데이터 구조를 생성)
이럴 경우 성능과 타입 안전성을 강화하지만 코드가 부풀어서 바이너리 크기가 커짐
https://ko.wikipedia.org/wiki/JIT_컴파일
프로그램을 실제 실행하는 시점 런타임에 기계어로 번역하는 컴파일 기법
동적 언어에서는 JIT 컴파일러가 실행 중인 타입에 맞춰 제네릭을 스페셜라이제이션을 구현합니다
이럴 경우 유연적이지만 런타임 오버헤드가 발생할 수 있다
스위프트는 성능과 유연성을 모두 갖길 원하여서 JIT이 아닌 AOT(Ahead-Of-Time) 방식을 사용해서 컴파일 타임에 가능한 모든 정보를 활용해 최적화된 바이너리를 생성
컴파일러는 제네릭 함수나 타입을 특정 타입에 대해 스페셜라이즈된 버전을 생성한다
만약 스위프트에서 Array< Int >와 Array< String >이 있다면 각각에 대해 최적화된 별도의 코드를 생성한다
제네릭 타입이 프로토콜 타입으로 사용되거나 타입 정보를 런타임에 유지해야 할 경우 스위프트는 타입 정보를 지워서 공통 인터페이스를 사용
특정 타입이 아닌 일반적인 제네릭 타입에 대해선 공유 코드를 사용하여 바이너리 크기를 줄이고, 재사용성을 높입니다.
예를 들어, Array가 특정 타입 없이 사용되면, 공유된 코드로 처리됩니다.
스위프트 컴파일러는 제네릭을 스위프트 중간 표현(SIL) 단계에서 최적화하는데
SIL은 제네릭 타입을 분석하여 어떤 경우에는 스페셜라이제이션을 수행하고, 다른 경우에는 타입 소거를 적용한다
컴파일된 SIL은 LLVM IR(중간 표현)로 변환되는데 LLVM 최적화 단계에서 추가적인 최적화가 수행하는데
여기서 불필요한 제네릭 호출 오버헤드가 제거 타입 정보가 가능한 경우 인라인화
스위프트는 런타임 성능을 극대화하기 위해서 컴파일 타임에 가능한 모든 정보를 활용해 최적화된 바이너리를 생성해서 최적화를 진행한다
ABI(Application Binary Interface) 가 안정적이여서 제네릭 코드를 스페셜라이즈하면서도 플랫폼 간 호환성을 유지한다
제네릭타입과 반대가 되는 타입으로
반환 타입을 명확히 하지 않는 경우에도 성능을 유지할 수 있도록 타입 정보 관리에 최적화를 적용한다
컴파일러가 타입의 구조와 구성 요소를 활용해 최적화하거나 안전성을 강화하는 데 사용하는 정보를 구조적 타입 정보라하는데 이 정보를 런타임에서 저장해두고 활용하여 불필요한 메모리 복사나 추가 연산을 제거한다
즉 스위프트 컴파일러는 런타임에 부담을 주지않기 위해 컴파일에 많은 것들을 해둔다 하지만..?